Compare commits

..

194 Commits

Author SHA1 Message Date
Paulus Schoutsen
fce275d29c Merge pull request #16787 from home-assistant/rc
0.78.3
2018-09-22 11:20:29 +02:00
Paulus Schoutsen
5eda5f2f7b Bumped version to 0.78.3 2018-09-22 11:18:54 +02:00
edif30
5ab580ab34 Bump gtts-token to 1.1.2 (#16775)
* bump gtts-token to 1.1.2

* bump gtts-token to 1.1.2

* bump gtts-token to 1.1.2
2018-09-22 11:18:47 +02:00
Paulus Schoutsen
2ac16b12c1 Merge pull request #16770 from home-assistant/rc
0.78.2
2018-09-21 11:40:04 +02:00
Paulus Schoutsen
44a98fb77a Bumped version to 0.78.2 2018-09-20 20:26:25 +02:00
Fabian Affolter
03d93bea34 Upgrade zeroconf to 0.21.2 (#16730) 2018-09-20 20:25:39 +02:00
Fabian Affolter
9dbac9b033 Upgrade zeroconf to 0.21.1 (#16687) 2018-09-20 20:25:38 +02:00
tadly
9e86f0498b Upgrade zeroconf to 0.21.0 (#16647) 2018-09-20 20:25:38 +02:00
Paulus Schoutsen
0d0d5c8c2c Merge pull request #16742 from home-assistant/rc
0.78.1
2018-09-20 13:55:09 +02:00
Paulus Schoutsen
c7d5f7698e Bumped version to 0.78.1 2018-09-20 11:32:26 +02:00
Jason Hu
9bbd61cbe4 Upgrade netdisco to 2.1.0 (#16735) 2018-09-20 11:32:16 +02:00
Jason Hu
7a7a164cb8 Handle chromecast CONNECTION_STATUS_DISCONNECTED event (#16732) 2018-09-20 11:32:15 +02:00
Andreas Oberritter
8379567636 SnmpSensor: Fix async_update (#16679) (#16716)
Bugfix provided by awarecan.
2018-09-20 11:32:14 +02:00
Paulus Schoutsen
9e59fc5d05 Merge pull request #16666 from home-assistant/rc
0.78.0
2018-09-17 19:03:37 +02:00
Paulus Schoutsen
366e270e94 Bump frontend to 20180916.0 2018-09-17 18:33:41 +02:00
Paulus Schoutsen
f918d62571 version bump to 0.78.0 2018-09-17 10:48:09 +02:00
Paulus Schoutsen
18ce5092b4 Bumped version to 0.78.0b3 2018-09-15 12:46:02 +02:00
Paulus Schoutsen
7f7372198a Update translations 2018-09-15 12:45:54 +02:00
Pascal Vizeli
abe61c5529 Rewrite bluetooth le (#16592)
* Rewrite bluetooth le

* Update requirements_all.txt

* Update gen_requirements_all.py

* Update bluetooth_le_tracker.py

* Update bluetooth_le_tracker.py

* Update bluetooth_le_tracker.py

* Update bluetooth_le_tracker.py

* Update bluetooth_le_tracker.py

* Update bluetooth_le_tracker.py
2018-09-15 12:45:23 +02:00
Jason Hu
b231fa2616 Fix broken bluetooth tracker (#16589) 2018-09-15 12:45:23 +02:00
Jason Hu
336289d7e7 Add retry limit for chromecast connection (#16471) 2018-09-15 12:45:22 +02:00
Paulus Schoutsen
a88cda44d9 Bumped version to 0.78.0b2 2018-09-12 13:30:14 +02:00
Zoé Bőle
08100a485a Increasing python-websockets' version number (#16578)
* increasing python-websockets' version number so now it works with python 3.7

* required version for websockets increased to work with Python 3.7

* script/gen_requirements_all.py is done
2018-09-12 13:30:04 +02:00
Jason Hu
4c1b816bb8 Track refresh token last usage information (#16408)
* Extend refresh_token to support last_used_at and last_used_by

* Address code review comment

* Remove unused code

* Add it to websocket response

* Fix typing
2018-09-12 13:30:04 +02:00
Jason Hu
1983361373 Return if refresh token is current used one in WS API (#16575) 2018-09-12 13:29:38 +02:00
Paulus Schoutsen
68c21530ca Update frontend to 20180912.0 2018-09-12 13:24:02 +02:00
Paulus Schoutsen
6d33fb2dc8 Bumped version to 0.78.0b1 2018-09-11 21:45:42 +02:00
Paulus Schoutsen
e50fc69c1e Add websocket commands for refresh tokens (#16559)
* Add websocket commands for refresh tokens

* Comment
2018-09-11 21:45:33 +02:00
Paulus Schoutsen
20f06f4eb9 Fix invalid state (#16558)
* Fix invalid state

* Make slightly more efficient in unsubscribing

* Use uuid4"
2018-09-11 21:45:32 +02:00
Paulus Schoutsen
37c8aac76f Fix typo (#16556) 2018-09-11 21:45:32 +02:00
Jason Hu
20e562b816 Long-lived access token (#16453)
* Allow create refresh_token with specific access_token_expiration

* Add token_type, client_name and client_icon

* Add unit test

* Add websocket API to create long-lived access token

* Allow URL use as client_id for long-lived access token

* Remove mutate_refresh_token method

* Use client name as id for long_lived_access_token type refresh token

* Minor change

* Do not allow duplicate client name

* Update docstring

* Remove unnecessary `list`
2018-09-11 21:45:31 +02:00
Zellux Wang
a4cec0b871 Fix arlo intilization when no base station available (#16529)
* Fix arlo intilization when no base station

* Fix pylint for empty camera check

* Fix typo

* Minor change to trigger CI again
2018-09-11 21:44:32 +02:00
vikramgorla
60b45d4ba5 bugfix - incorrect camera type and missing sensors when multiple netatmo cameras (#16490)
fixed get_camera_type as it was originally not consuming any input, was looping with all cameras and the first camera type was retutned, modified to call cameraType using provided camera name.
2018-09-11 21:44:31 +02:00
Tom Harris
1ea8c1ece3 Fix insteon Hub v1 support (#16472)
* Fix support for Hub version 1 (i.e. pre-2014 Hub model 2242)

* Bump insteonplm to 0.14.1

* Code review changes

* Clean up and better document set_default_port

* Simplify set_default_port based on code review

* Remove Callable type import

* Simplify port setup
2018-09-11 21:44:31 +02:00
Paulus Schoutsen
44d210698e Fail fetch auth providers if onboarding required (#16454) 2018-09-11 21:44:30 +02:00
Diogo Gomes
968809c991 Replace api_password in Camera.Push (#16339)
* Use access_token and user provided token instead of api_password

* address comments by @awarecan

* new tests

* add extra checks and test

* lint

* add comment
2018-09-11 21:44:30 +02:00
Paulus Schoutsen
b9b8bc678c Update frontend to 20180911.0 2018-09-11 21:37:49 +02:00
Paulus Schoutsen
00d8907c5e Update translations 2018-09-11 21:37:05 +02:00
Franck Nijhof
7a5e828f6b Updates documentation repo URL in PR template (#16537) 2018-09-10 14:28:21 +02:00
Paulus Schoutsen
3f97944f6b Bumped version to 0.78.0b0 2018-09-10 13:44:56 +02:00
Paulus Schoutsen
1d6609e386 Update translations 2018-09-10 13:44:27 +02:00
Paulus Schoutsen
fb15dbf3ba Bump frontend to 20180910.0 2018-09-10 13:44:11 +02:00
Fabian Affolter
c20f147949 Upgrade keyring to 15.0.0 (#16536) 2018-09-10 13:35:47 +02:00
Fabian Affolter
8ed4d732ac Upgrade youtube_dl to 2018.09.10 (#16534) 2018-09-10 13:35:24 +02:00
Louis-Dominique Dubeau
a1578e3c6e Add a base_url configuration setting to tts. (#16478)
* Add a base_url configuration setting to tts.

* Remove the empty string as default value for base_urls

As requested in

https://github.com/home-assistant/home-assistant/pull/16478#pullrequestreview-153526144
2018-09-10 11:50:25 +02:00
Jason Hu
253e787a1b Upgrade aiohttp to 3.4.4 (#16486) 2018-09-10 10:39:51 +02:00
Fabian Affolter
0d7ee9b93b Order imports (#16515) 2018-09-09 14:26:06 +02:00
tadly
d6d4ff6888 adds listener for OnAVStart and OnAVChange (#16495)
With Kodi 18, OnPlay is called before an item actually started playing.
For this OnAVStart and OnAVChange have been introduced.
2018-09-09 09:55:57 +02:00
Jason Hu
4291bdc6b2 Move voluptuous-serialize to core requirement (#16507) 2018-09-09 09:49:51 +02:00
Anders Melchiorsen
7d590a6b93 Update pyHS100 to 0.3.3 (#16502) 2018-09-09 00:34:22 +02:00
Ville Skyttä
e3e3ed42ec Fix Netgear LTESensor docstring (#16501) 2018-09-08 23:22:41 +02:00
Paulus Schoutsen
e7b8d2e6df Update name legacy api password (#16455) 2018-09-08 22:10:42 +02:00
Fabian Affolter
9944c60311 Check if API key is valid and users available (#16494) 2018-09-08 18:33:41 +02:00
Varga Tamas
93143384a8 Restore status attribute for xiaomi_vacuum (#16366)
* Added new states and exposed state/state code received from xiaomi vacuum

* Restore status attribute for xiaomi_vacuum
2018-09-08 18:13:24 +02:00
Florian Werner
8a2bc99f63 Add rate of change to statistics sensor (#15632)
* always export max_age/min_age

* downgrade errors of missing data
on start with empty recorder database these errors are logged multiple times:
ERROR (MainThread) [homeassistant.components.sensor.statistics] mean requires at least one data point
ERROR (MainThread) [homeassistant.components.sensor.statistics] variance requires at least two data points

downgrade them to debug as they are not meaningful to end users

* add change_rate attribute
this calculates the average change rate of all data points

* simplify count, reorder attribute calculation

* reorder initialization

* reorder attribute names

* don't use min/max for min_age/max_age

* add test case

* style

* style

* sort constants

* init variables with None

* add precision config setting

* round to precision

* test round
2018-09-08 01:10:08 +02:00
Andreas Oberritter
50266e9b91 Support SNMPv3 and asyncio in snmp sensor (#14753)
* snmp sensor: Add asyncio support and reuse SnmpEngine object

* snmp sensor: Support protocol v3

* Fix lint issue
2018-09-07 20:51:18 +02:00
mvn23
7ad094b0a7 Add OpenTherm Gateway climate platform (#16299)
* Initial OpenTherm Gateway support.

* Fix coveragerc and requirements_all*.txt
Overall cleanup and polishing

* Make hound/flake/travis happy

* Basic improvements to comply with Home Assistant's style guidelines
Changed wording from "component" to more appropriate terms where necessary
Fixed small mistakes that snuck in during testing and/or due to my own ignorance ;)

* Fixed overwriting state property

* Fixed a bug with ROOM_SETPOINT_OVRD
Updated dependency pyotgw to latest version

* Remove unit_of_measurement from OpenThermGateway class

* Cleanup after previous commits

* Moved initialisation and configuration from async_setup_platform to async_added_to_hass

* Make travis happy

* Disable polling for this platform
Improve update flow

* Small improvements/optimisations
2018-09-07 18:16:19 +02:00
Fabian Affolter
a5715c48a4 Fix GitHub change to resolve conflicts (#16477) 2018-09-07 14:41:31 +02:00
Andreas Oberritter
d0f9d125a7 Support SNMPv3 and asyncio in snmp switch (#14754)
* snmp switch: Add asyncio support and reuse SnmpEngine object

* snmp switch: Support protocol v3
2018-09-07 13:28:42 +02:00
cpw
80c77b8696 Update radiotherm (#15031)
the two device_state_attributes don't return the current actual state (fan on/off, heat/cool), but rather the mode of the fan or thermostat (generally auto). As such this change makes the actual current state visible to the system, so you can do stuff when the fan turns on, for example. I suspect this is just a bug, but maybe it was intended NOT to be able to see the actual fan state?
2018-09-07 13:21:32 +02:00
Dom
1f73840aab Add Yale Smart Alarm component (#16377)
* Add Smart Alarm Component

* Lint fixes

* Update coverage

* Update coverage with the correct file name

* PR Fixes. Update client version (to include timeouts). Cleaned up clien tcreation and improved authentication failure error. Added state map to replace nasty if blocks.

* PR Fixes. Update client version (to include timeouts). Cleaned up clien tcreation and improved authentication failure error. Added state map to replace nasty if blocks.

* Remove test file added in error.

* PR Fixes
2018-09-07 12:22:10 +02:00
Greg Laabs
fbaa489533 Update license to official GitHub template (#16470)
* Delete LICENSE.md

* Create LICENSE.md
2018-09-07 10:11:51 +02:00
Tsvi Mostovicz
cff9b1bf7e Fix waze_travel_time component startup (#16465)
* Fix waze_travel_time component startup

* Remove @Throttle decorator as per pvizelli's request

* Make the linting gods happy again
2018-09-07 09:55:53 +02:00
Ioan Loosley
ce06229c42 Added Twitch v5 support to the twitch platform (#16428)
* code not working

* did it all all at once

* removing debug stuff

* fixing lint issues

* fixing lint issues

* I am blind

* I felt small(80 by somthing) was to small, especily with lovelace

* fixing silly mistake

* this shouldnt be here

* made some changes

* corrrected client_id

* corrrected client_id

* less returns needed

* Fixing Small bugs and making stuff snake case

* Tweaking
2018-09-06 14:08:39 +02:00
Daniel Høyer Iversen
3acb3a86cf update rfxtrx lib (#16463) 2018-09-06 12:50:55 +02:00
Fabian Affolter
db1bda2975 Upgrade Sphinx to 1.7.8 (#16459) 2018-09-06 12:31:30 +02:00
Fabian Affolter
2640db1522 Upgrade shodan to 1.10.1 (#16460) 2018-09-06 12:31:08 +02:00
PhracturedBlue
cf4b72e00e Fix camera proxy to not require api_password to function (#16450) 2018-09-06 10:51:16 +02:00
Daniel Høyer Iversen
5bd9be6252 switchbot (#16396)
* switchbot

* style

* use switchbot lib

* unnecessary import

* switchbot, rename variable update requirements_all

* add line
2018-09-06 10:02:37 +02:00
kbickar
e1084e3953 Upgrade sense library to 0.4.2 (#16429)
* Added error handling for sense API timeouts

* Moved imports in function

* Moved imports to more appropriate function

* Change exception to custom package version

* Updated sense_energy library to 0.4.2
2018-09-05 08:01:36 +02:00
Jason Hu
3afc983c05 Fix openuv.config_flow unit test (#16419) 2018-09-04 21:18:35 +02:00
Paulus Schoutsen
746f4ac158 Add context to scripts and automations (#16415)
* Add context to script helper

* Update script component

* Add context to automations

* Lint
2018-09-04 21:16:24 +02:00
Paul Annekov
e1501c83f8 Fix Mi Flora median calculation (#16085)
* fixed median was based on 1.5 minute interval, not 1 hour

* ignore median and set state from first value, when previous state was None

* update before add, removed unused 'retries' and 'ble_timeout', check if platform ready

* added missing blank line

* fixed too long line

* using modern python 3.5 features, changed comment to be less verbose

* continuation line fix

* removed DEFAULT_SCAN_INTERVAL in favor of existing SCAN_INTERVAL
2018-09-04 21:03:30 +02:00
Toon Willems
3bd12fcef6 Implement correct state for RFlink cover (#16304)
* implement correct state for rflink cover

* Fix linting error

* invert logic as local testing pointed out it should be reversed

* add period at the end to satisfy the linter
2018-09-04 11:15:02 +02:00
Daniel Høyer Iversen
85658b6dd1 Clean up dlink and some bug fix (#16346)
* Update dlink.py

* style

* style
2018-09-04 10:50:12 +02:00
Rohan Kapoor
e61ac1a4a1 Delegate mqtt topic match validation to the paho mqtt client (#16403)
* Delegate mqtt match topics to the paho mqtt client

* Fixing linting error with importing MQTTMatcher
2018-09-04 10:31:45 +02:00
Robert Svensson
8fa9992589 Service to load new deCONZ devices without restart (#16308)
* New service to load new devices from deCONZ without restarting HASS

* Do not use len to check if list is empty

* Add support for scenes to be updated as well

* Rework refresh devices method

* Fix test
2018-09-04 09:24:42 +02:00
Aaron Bach
f96aee2832 Add config flow for OpenUV (#16159)
* OpenUV config flow in place

* Test folder in place

* Owner-requested comments

* Tests

* More tests

* Owner-requested changes (part 1 of 2)

* Updated requirements

* Owner-requested changes (2 of 2)

* Removed unnecessary import

* Bumping Travis

* Updated requirements

* More requirements

* Updated tests

* Owner-requested changes

* Hound

* Updated docstring
2018-09-04 09:22:44 +02:00
Robert Svensson
7a6facc875 Device and entity registry remove config entry on unload (#16247)
* Test

* Ability to remove device

* Don't remove devices, instead remove config entry from device and entity registries

* Remove print

* Remove is not the same as unload

* Add tests

* Fix hound comment
2018-09-04 09:00:14 +02:00
9R
7ea482cb1d add ExpressBus icon key to sensor.mvg (#16387) 2018-09-04 08:48:03 +02:00
Rene Nulsch
a4aa30fc73 Fix SystemMonitor IP address sensor (#16394) 2018-09-04 08:46:04 +02:00
Russell Cloran
ba63a6abc0 zha: Bump to zigpy 0.2.0/bellows 0.7.0 (#16404) 2018-09-03 22:46:27 -07:00
Daniel Høyer Iversen
2252f4a257 Bug fix for Tibber (#16397) 2018-09-04 01:11:40 +02:00
Pawel
00cba29ae1 Support for playing radio preset by Onkyo media_player (#16258)
* Added support to play radio preset by play media

* Switch radio station by preset for Onkyo

* added SUPPORT_PLAY_MEDIA
2018-09-03 21:40:04 +02:00
Paulus Schoutsen
cd473643fe Merge branch 'master' into dev 2018-09-03 14:17:00 +02:00
Paulus Schoutsen
4063b24ddb Merge pull request #16390 from home-assistant/rc
0.77.3
2018-09-03 14:15:54 +02:00
Paulus Schoutsen
46734b8409 Bumped version to 0.77.3 2018-09-03 13:22:49 +02:00
Paulus Schoutsen
b9c80a6bb3 Update translations 2018-09-03 13:21:47 +02:00
Paulus Schoutsen
a2a447b466 Update translations 2018-09-03 13:21:37 +02:00
Paulus Schoutsen
669b3458b9 Update frontend to 20180903.0 2018-09-03 13:21:16 +02:00
Paulus Schoutsen
bf29cbd381 Update frontend to 20180903.0 2018-09-03 13:20:57 +02:00
Daniel Høyer Iversen
1966597d5e add_entities for switchmate (#16368) 2018-09-02 23:05:48 +02:00
Jason Hu
4685a2cd97 Update server.py (#16375) 2018-09-02 22:17:29 +02:00
Fabian Affolter
78fcea25bb Upgrade attrs to 18.2.0 (#16372) 2018-09-02 19:01:43 +02:00
Fabian Affolter
ac3700d1c4 Upgrade python-telegram-bot to 11.0.0 (#16373) 2018-09-02 19:01:25 +02:00
Totoo
03480dc779 Update discord.py (#16248)
* Update discord.py

* Update discord.py

Fixed ATTR_IMAGES checking, line length, and ATTR_DATA imported. Also fixed missing spaces.

* Update discord.py

Fix E302...
2018-09-02 18:58:31 +02:00
Martin Fuchs
a5cff9877e Add support for Tahoma Lighting Receiver on/off io (#15925)
* Add support for Tahoma light switch

* Clean up attributes and add available method

* Remove else statement
2018-09-02 17:02:51 +02:00
Lev Aronsky
b29c296ced Generic Thermostat: add support for climate.turn_on/climate.turn_off (#16080)
* Added async_turn_on and async_turn_off implementations.

* Added turning on/off tests to generic thermostat

* style

* style

* style
2018-09-02 16:42:08 +02:00
MarcSN311
357e5eadb8 Added 'nomapnt', 'outcurnt', 'loadapnt' fields (#16176)
* Added 'nomapnt', 'outcurnt', 'loadapnt' fields

Also added Ampere and Volt-Ampere to INFERRED_UNITS

* Fix lint issue
2018-09-02 15:51:15 +02:00
Fabian Affolter
52e922171d Upgrade to youtube_dl to 2018.09.01 (#16365) 2018-09-02 14:33:20 +02:00
Fabian Affolter
97695a30f5 Upgrade shodan to 1.10.0 (#16363) 2018-09-02 12:51:56 +02:00
Fabian Affolter
1d12c7b0e7 Upgrade mutagen to 1.41.1 (#16361) 2018-09-02 12:51:36 +02:00
Fabian Affolter
15ad82b9bd Upgrade qnapstats to 0.2.7 (#16360) 2018-09-02 12:51:25 +02:00
Fabian Affolter
03fb2b32a6 Upgrade Sphinx to 1.7.7 (#16359) 2018-09-02 12:51:07 +02:00
Jason Hu
87eb6cd25a Upgrade hbmqtt to 0.9.4 (#16356)
* Upgrade to hbmqtt 0.9.4

* Lint

* Typo
2018-09-02 12:50:30 +02:00
Tod Schmidt
3797b6b012 Snips: Added special slot values, session_id and slotname_raw (#16185)
* Added special slot values, site_id, session_id, and slotname_raw

* Update snips.py
2018-09-02 00:01:11 +02:00
Jesse Rizzo
2b0b431a2a Update to EnvoyReader 0.2, support for more hardware (#16212)
* Add support for older Envoy models

* Stop requiring envoy model name in config

* Update to envoy_reader0.2

* Minor formatting fixes

* run script/gen_requirements_all.py

* Minor formatting fixes

* Change some strings to constants, use getattr to call function
2018-09-01 23:45:47 +02:00
Maciej Bieniek
e75a1690d1 Add unique_id to MQTT Light (#16303)
* Add unique_id

* Delete whitespaces
2018-09-01 23:37:03 +02:00
Joshi
444df5b09a Add support for sound_mode for Yamaha rxv media_player (#16352) 2018-09-01 23:34:38 +02:00
Daniel Høyer Iversen
b31890c4cb Handle netatmo exception (#16344) 2018-09-01 23:30:34 +02:00
Phil Bruckner
a5d95dfbdc Make last_seen attribute a timezone aware datetime in UTC (#16348)
The last_seen attribute was a datetime in the local timezone but with
no tzinfo (i.e., a "naive" datetime.) When state changes occurred it
would be printed incorrectly in homeassistant.log because
homeassistant.util.dt.as_local assumes any datetime without tzinfo is
UTC. Also most, if not all, datetime attributes are timezone aware in
UTC. So use homeassistant.util.dt.as_utc (which assumes a naive
datetime is local) to convert last_seen to a timezone aware datetime
in UTC.
2018-09-01 18:49:03 +02:00
Philipp Temminghoff
901cfef78e Support Sonos Beam HDMI input (#16340) 2018-09-01 16:02:38 +02:00
Daniel Perna
2c7d6ee6b5 Fix missing humidity sensor (#16337) 2018-09-01 10:40:16 +02:00
Tom Harris
d3791fa45d Add Cover to the Insteon component (#16215)
* Create cover platform

* Create insteon cover platform

* Bump insteonplm to 0.13.0

* Change async_add_devices to async_add_entities

* Missing doc string

* Simplify open and set_position

* Flake8

* Bump insteonplm to 0.13.1

* Code review changes

* Flake8 updates
2018-08-31 23:56:26 +02:00
Matt Schmitt
fa81385b5c Add unique ID (#16323) 2018-08-31 16:47:37 +02:00
thomaslian
7d852a985c Upgrade Adafruit-DHT to 1.3.4 (#16327)
* Update dht.py

* Update requirements_all.txt
2018-08-31 16:47:10 +02:00
Paulus Schoutsen
5e8a1496d7 Update translations 2018-08-31 13:23:22 +02:00
Paulus Schoutsen
efa9c82c38 Update frontend to 20180831.0 2018-08-31 12:59:39 +02:00
lamiskin
93f45779c6 Correct wemo static device discovery issue. (#16292)
A recent change caused an issue if a single static wemo device is offline and could not be reached, then the whole component would not initialize (and therefore all other wemo devices are not added).
2018-08-31 12:57:07 +02:00
Malte Franken
26d39d39ea avoid error in debug log mode and rss entry without title (#16316) 2018-08-31 12:54:25 +02:00
Anders Melchiorsen
b43c47cb17 Fix LIFX effects (#16309) 2018-08-31 10:17:11 +02:00
PhracturedBlue
67d8db2c9f Use asterisk_mbox 0.5.0 client (#16296) 2018-08-30 18:44:37 +02:00
Teemu R
3cbf8e4f87 Bump songpal dependency (#16297)
Fixes #14936
2018-08-30 18:21:37 +02:00
Malte Franken
f20a3313b0 Geo Location component (#15953)
* initial working version of a geo location component and georss platform

* ensure that custom attributes don't override built-in ones

* bugfixes and tests

* fixing tests because of introduction of new component using same fixture

* improving test cases

* removing potentially unavailable attribute from debug message output

* completing test suite

* cleaning up debug messages; sorting entries in group view by distance

* ability to define the desired state attribute and corresponding unit of measurement; sort devices in group by configured state; find centroid for map if event is defined by polygon; updated tests

* sort entries in group; code clean-ups

* fixing indentation

* added requirements of new component and platform

* fixed various lint issues

* fixed more lint issues

* introducing demo geo location platform; refactored geo location component and geo rss platform to fit

* removing geo rss events platform; added unit tests for geo location platform and demo platform

* reverting change in debug message for feedreader to avoid confusion with new geo location component

* updated requirements after removing georss platform

* removed unused imports

* fixing a lint issue and a test case

* simplifying component code; moving code into demo platform; fixing tests

* removed grouping from demo platform; small refactorings

* automating the entity id generation (the use of an entity namespace achieves the same thing)

* undoing changes made for the georss platform

* simplified test cases

* small tweaks to test case

* rounding all state attribute values

* fixing lint; removing distance from state attributes

* fixed test

* renamed add_devices to add_entities; tweaked test to gain more control over the timed update in the demo platform

* reusing utcnow variable instead of patched method

* fixed test by avoiding to make assumptions about order of list of entity ids

* adding test for the geo location event class
2018-08-30 13:58:23 +02:00
Robbie Trencheny
54c3f4f001 Fix spelling mistake in recorder migration [ci skip] 2018-08-29 14:59:48 -07:00
Paulus Schoutsen
7289d5b656 Merge branch 'master' into dev 2018-08-29 23:37:53 +02:00
Robert Svensson
645c3a67d8 Fix so that entities are properly unloaded with config entry (#16281) 2018-08-29 23:18:20 +02:00
Conrad Juhl Andersen
88f72a654a Fix error when vacuum is idling (#16282) 2018-08-29 23:17:18 +02:00
Paulus Schoutsen
87df102772 Bump frontend to 20180829.1 2018-08-29 22:59:55 +02:00
Sebastian Muszynski
25ee8e551c Fix data_key override by parent class (#16278) 2018-08-29 22:29:34 +02:00
Pavel Pletenev
99d48795b9 Add support for Habitica (#15744)
* Added support for Habitica

Second refactoring
Moved all config to component.
Sensors are autodiscovered.
Signed-off-by: delphi <cpp.create@gmail.com>

* Apply requested changes

Signed-off-by: delphi <cpp.create@gmail.com>

* Made event fire async. Made `sensors` config implicit and opt-out-style.

Signed-off-by: delphi <cpp.create@gmail.com>

* Removed unneeded check and await.

Signed-off-by: delphi <cpp.create@gmail.com>

* Moved into separate component package and added service.yaml

Signed-off-by: delphi <cpp.create@gmail.com>

* Fix coveralls

Signed-off-by: delphi <cpp.create@gmail.com>
2018-08-29 21:13:01 +02:00
Jason Hu
5681fa8f07 Nest Thermostat has software version (#16275) 2018-08-29 13:00:40 -06:00
Paulus Schoutsen
867d17b03d Add Hue device info (#16267)
* Add Hue device info

* Set with tuples

* Fix tests
2018-08-29 17:04:04 +02:00
Paulus Schoutsen
7751dd7535 Add device info Nest (#16265)
* Add device info Nest

* Sets
2018-08-29 16:44:10 +02:00
Paulus Schoutsen
16a885824d Add device info for sonos (#16263)
* Add device info for sonos

* Sets
2018-08-29 16:27:08 +02:00
Paulus Schoutsen
3934f7bf3a Add device info to Chromecast (#16261) 2018-08-29 15:46:09 +02:00
cgtobi
96cf6d59a3 Replace Authorization by Authentication (#16259) 2018-08-29 15:43:01 +02:00
Diogo Gomes
d46a1a266d bump version (#16262) 2018-08-29 15:32:47 +02:00
Matt Schmitt
aaa1ebeed5 Add support for discrete states to MyQ cover (#16251)
* Add discrete states and update dependency

* Add translation dict
2018-08-29 14:33:09 +02:00
Daniel Høyer Iversen
18ba50bc2d Switchmate (#15535)
* Switchmate

* switchmate

* swithcmate

* switchmate

* switchmate

* fix comments

* Update switchmate.py

* change error log
2018-08-29 12:56:15 +02:00
Paulus Schoutsen
3df8840fee Version bump to 0.78.0.dev0 2018-08-29 12:20:05 +02:00
Paulus Schoutsen
630b5df59a Merge branch 'master' into dev 2018-08-29 12:19:30 +02:00
Paulus Schoutsen
e8801ee22f Update translations 2018-08-29 10:28:34 +02:00
Paulus Schoutsen
74c0429437 Bump frontend to 20180829.0 2018-08-29 10:27:34 +02:00
Jason Hu
563588651c Tweak MFA login flow (#16254)
* Tweak MFA login flow

* Fix typo
2018-08-29 10:16:54 +02:00
Robert Svensson
63614a477a def device shouldnt call it self but self._device (#16255) 2018-08-29 10:07:32 +02:00
Paulus Schoutsen
f891d0f5be Update translations 2018-08-28 20:55:58 +02:00
Jason Hu
257b8b9b80 Blow up startup if init auth providers or modules failed (#16240)
* Blow up startup if init auth providers or modules failed

* Delete core.entity_registry
2018-08-28 20:54:01 +02:00
Paulus Schoutsen
9a786e449b Fix hangouts (#16232) 2018-08-28 15:44:06 +02:00
Paulus Schoutsen
09dc4d663d Improve package loadable (#16237)
* Add caching to package loadable

* Fix tests

* Improve package loadable

* Lint

* Typing
2018-08-28 12:52:18 +02:00
Paulus Schoutsen
12709ceaa3 Avoid insecure pycryptodome (#16238) 2018-08-28 12:49:50 +02:00
Jason Hu
67df162bcc Change log level to error when auth provider failed loading (#16235) 2018-08-28 11:23:58 +02:00
Paulus Schoutsen
a14980716d Package loadable: compare case insensitive (#16234) 2018-08-28 10:53:12 +02:00
Paulus Schoutsen
376d4e4fa0 Warning missed a space (#16233) 2018-08-28 09:32:50 +02:00
Paulus Schoutsen
5397c0d73a Update trusted networks flow (#16227)
* Update the trusted networks flow

* Fix tests

* Remove errors
2018-08-28 00:37:15 +02:00
Robert Svensson
8ab31fe139 Store devices as dict instead of list (#16229)
* Store devices as dict instead of list

* Use OrderedDict
2018-08-28 00:37:04 +02:00
Marcel Hoppe
45649824ca rewrite hangouts to use intents instead of commands (#16220)
* rewrite hangouts to use intents instead of commands

* small fixes

* remove configured_hangouts check and CONFIG_SCHEMA

* Lint

* add import from .config_flow
2018-08-28 00:20:12 +02:00
Paulus Schoutsen
6f0c30ff84 Bump frontend to 20180827.0 2018-08-27 22:28:17 +02:00
Fabian Affolter
943260fcd6 Upgrade alpha_vantage to 2.1.0 (#16217) 2018-08-27 22:00:20 +02:00
Paulus Schoutsen
24aa580b63 Fix device telldus (#16224) 2018-08-27 21:56:28 +02:00
Daniel Bowman
8435d2f53d openalpr flag WITH_TEST should be WITH_TESTS (#16218)
Removes warning from openalpr build and saves a few seconds from build
time as tests weren't being bypassed as intended
2018-08-27 17:17:43 +02:00
Fabian Affolter
c51170ef6d Add Volkszaehler sensor (#16188)
* Add Volkszaehler sensor

* Update icons

* Improve code
2018-08-27 15:05:36 +02:00
Paulus Schoutsen
9d491f5322 Change auth warning (#16216) 2018-08-27 10:37:03 +02:00
Paulus Schoutsen
94662620e2 Update translations 2018-08-27 10:16:59 +02:00
Paulus Schoutsen
f1e378bff8 Add new translations 2018-08-27 09:48:17 +02:00
Julian Kahnert
2e9db1f5c4 Fix geizhals price parsing (#15990)
* fix geizhals price parsing

* Fix lint issue

* switch to the geizhals pypi package

* throttle updates

* update geizhals version

* initialize empty device

* minor changes to trigger another TravisCI test

* device => _device

* bump geizhals version
2018-08-27 09:39:11 +02:00
Hunter Horsman
dec2d8d5b0 Add device_tracker.bluetooth_update service (#15252)
* Add device_tracker.bluetooth_update service

Will immediately scan for Bluetooth devices outside of the interval timer. Allows for less frequent scanning, with scanning on demand via automation.

* remove excess whitespace per bot comments

* Refactored update_bluetooth to call new function update_bluetooth_once

* Change service name to bluetooth_tracker_update to reflect platform name

* Reformat for line length

* Linting fix, pydoc, first line should end with a period

* Fixed a method call, and removed some more unsused parameters
2018-08-27 09:08:23 +02:00
Jonas Karlsson
a439690bd7 Rewrite of Trafikverket weather - Multiple sensor types supported (#15935)
* Added precipitation type from API

Enables users to see type of precipitation.
Value returned from API is a string in swedish.

* Corrected tox verification errors

Correction of tox findings

* Missed in tox - fixed

* Hound witespace fix

* Updated comment to trigger travis rebuild

Travis tox failed due to problem with tox build process. 
Correcting in a comment to trigger retry in travis..

* Try to retrigger travis/tox successful rebuild

* Cleaning

* Cleaning more

* Trafikverket rebuilt for library

Extended pytrafikverket with weather sensor collction
Changed behaviour of sensor component to use pytrafikverket.
Added more sensors.

User need to change config to use new version.
 [] Documentation needs to be updated

* Cleaned up based on Martins input

Appreciate the feedback
2018-08-27 06:19:51 +02:00
Maikel Punie
5d7a2f92df Add temperature sensors to the velbus component (#16203)
* Added support for velbus temperature sensors

* Bumped the required version

* updated requirements_all.txt

* Auto review comments fixed

* Updated after comments

* Updated after comments

* Fix travis

* Fix travis
2018-08-27 06:06:46 +02:00
Paulus Schoutsen
4da719f43c Update translations 2018-08-26 22:52:21 +02:00
Matt Hamilton
bacecb4249 Replace pbkdf2 with bcrypt (#16071)
* Replace pbkdf2 with bcrypt

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

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

* WIP: add hash upgrade mechanism

* WIP: clarify decode issue

* remove stale testing code

* Fix test

* Ensure incorrect legacy passwords fail

* Add better invalid legacy password test

* Lint

* Run tests in async scope
2018-08-26 22:50:31 +02:00
Jason Hu
47755fb1e9 Add Time-based Onetime Password Multi-factor Authentication Module (#16129)
* Add Time-based Onetime Password Multi-factor Auth

Add TOTP setup flow, generate QR code

* Resolve rebase issue

* Use svg instead png for QR code

* Lint and typing

* Fix translation

* Load totp auth module by default

* use <svg> tag instead markdown image

* Update strings

* Cleanup
2018-08-26 22:38:52 +02:00
Penny Wood
69d104bcb6 Update aiohttp to version 3.4.0. (#16198) 2018-08-26 21:35:06 +02:00
Paulus Schoutsen
b043ac0f7f Update frontend to 20180826.0 2018-08-26 21:30:14 +02:00
PhracturedBlue
499bb3f4a2 Handle exception from pillow (#16190) 2018-08-26 21:29:15 +02:00
Marcel Hoppe
d166f2da80 remove hangouts.users state, simplifies hangouts.conversations (#16191) 2018-08-26 21:28:42 +02:00
Antoine GRÉA
3032de1dc1 Inconsistent entity_id when multiple sensors (#16205)
* Inconsistent entity_id when multiple sensors

I am submitting a change to fix a [bug](https://github.com/home-assistant/home-assistant/issues/16204) for when there are several sensors for the same hostname. For example I want to track my IPv4 and IPv6 address. It creates two entities that regularly switch ids based on the order they get initialized.

To fix this I comform to the way other componnents have addressed the issue by adding an optional `name` attribute.

* Line too long

* Removing trailing whitespace
2018-08-26 21:27:03 +02:00
Robert Svensson
5341785aae Revert changes to platforms using self.device (#16209)
* Revert tank_utility

* Fix Soundtouch

* Fix Plex

* Fix Emby

* Fix Radiotherm

* Fix Juicenet

* Fix Qwikswitch

* Fix Xiaomi miio

* Fix Nest

* Fix Tellduslive

* Fix KNX
2018-08-26 21:25:39 +02:00
Martin Fuchs
289b1802fd Add battery warning, rssi level and check for availability (#16193) 2018-08-26 21:20:34 +02:00
Fabian Affolter
0da3e73765 Upgrade sqlalchemy to 1.2.11 (#16192) 2018-08-26 12:28:44 +02:00
Dan Klaffenbach
0a7055d475 homematic: Make device avilable again when UNREACH becomes False (#16202) 2018-08-26 12:00:20 +02:00
Matthias Urlichs
a1ce14e70f MQTT: Log transmitted as well as received messages (#16195) 2018-08-26 10:04:51 +02:00
Thomas Delaet
2f2bcf0058 update python-velbus library version (#16194) 2018-08-25 20:42:26 +02:00
djm300
f929c38e98 Zoneminder SSL fix (#16157)
* Update zoneminder.py

Added a verify_ssl parameter for zoneminder

* PEP8 fixup

* PEP8 indenting fix

* Fix lint issue

* Remove whitespace
2018-08-25 11:21:57 +02:00
Paulus Schoutsen
617802653f Bump frontend to 20180825.0 2018-08-25 11:15:01 +02:00
Jason Hu
26a485d43c Default load trusted_network auth provider if configured trusted networks (#16184) 2018-08-25 11:09:48 +02:00
Paulus Schoutsen
456aa5a2b2 Fix hangouts (#16180) 2018-08-25 11:01:32 +02:00
Robert Svensson
97173f495c Device registry store config entry (#16152)
* Allow device registry to optionally store config entries

* Connections and identifiers are now sets with tupels

* Make config entries mandatory

* Fix duplicate keys in test

* Rename device to device_info

* Entity platform should only create device entries if config_entry_id exists

* Fix Soundtouch tests

* Revert soundtouch to use self.device

* Fix baloobs comments

* Correct type in test
2018-08-25 10:59:28 +02:00
Jason Hu
24a8d60566 Tweak log level for bearer token warning (#16182) 2018-08-25 07:57:36 +02:00
Fabian Affolter
69cea6001f Add 'moon_phase' to Dark Sky sensor (#16179) 2018-08-24 17:05:53 -06:00
Nate Clark
647b3ff0fe Decouple Konnected entity setup from discovery (#16146)
* decouple entity setup from discovery

* validate that device_id is a full MAC address
2018-08-24 23:29:25 +02:00
Nate Clark
84365cde07 fix error message for cv.matches_regex (#16175) 2018-08-24 23:27:12 +02:00
Robert Svensson
e91a1529e4 deCONZ - Support device registry (#16115)
Add support for device registry in deCONZ component
2018-08-24 19:37:22 +02:00
Jason Hu
e8775ba2b4 Add multi-factor auth module setup flow (#16141)
* Add mfa setup flow

* Lint

* Address code review comment

* Fix unit test

* Add assertion for WS response ordering

* Missed a return

* Remove setup_schema from MFA base class

* Move auth.util.validate_current_user -> webscoket_api.ws_require_user
2018-08-24 10:17:43 -07:00
282 changed files with 5793 additions and 2119 deletions

View File

@@ -116,11 +116,14 @@ omit =
homeassistant/components/google.py
homeassistant/components/*/google.py
homeassistant/components/habitica/*
homeassistant/components/*/habitica.py
homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
homeassistant/components/*/hangouts.py
homeassistant/components/*/hangouts.py
homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py
@@ -142,12 +145,12 @@ omit =
homeassistant/components/ihc/*
homeassistant/components/*/ihc.py
homeassistant/components/insteon/*
homeassistant/components/*/insteon.py
homeassistant/components/insteon_local.py
homeassistant/components/insteon_plm.py
homeassistant/components/ios.py
@@ -225,7 +228,7 @@ omit =
homeassistant/components/opencv.py
homeassistant/components/*/opencv.py
homeassistant/components/openuv.py
homeassistant/components/openuv/__init__.py
homeassistant/components/*/openuv.py
homeassistant/components/pilight.py
@@ -374,6 +377,7 @@ omit =
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/alarm_control_panel/totalconnect.py
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
homeassistant/components/apiai.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/concord232.py
@@ -411,6 +415,7 @@ omit =
homeassistant/components/climate/honeywell.py
homeassistant/components/climate/knx.py
homeassistant/components/climate/oem.py
homeassistant/components/climate/opentherm_gw.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/climate/sensibo.py
@@ -759,6 +764,7 @@ omit =
homeassistant/components/sensor/uscis.py
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/volkszaehler.py
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/waze_travel_time.py
homeassistant/components/sensor/whois.py
@@ -789,6 +795,8 @@ omit =
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/snmp.py
homeassistant/components/switch/switchbot.py
homeassistant/components/switch/switchmate.py
homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py

View File

@@ -3,7 +3,7 @@
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
## Example entry for `configuration.yaml` (if applicable):
```yaml
@@ -15,7 +15,7 @@
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
If the code communicates with devices, web services, or third-party tools:
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).

View File

@@ -1,194 +1,201 @@
Apache License
==============
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
### Terms and Conditions for use, reproduction, and distribution
1. Definitions.
#### 1. Definitions
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
Work shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
#### 2. Grant of Copyright License
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
#### 3. Grant of Patent License
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
#### 4. Redistribution
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
#### 5. Submission of Contributions
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
#### 6. Trademarks
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
#### 7. Disclaimer of Warranty
END OF TERMS AND CONDITIONS
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
APPENDIX: How to apply the Apache License to your work.
#### 8. Limitation of Liability
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
Copyright [yyyy] [name of copyright owner]
#### 9. Accepting Warranty or Additional Liability
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
http://www.apache.org/licenses/LICENSE-2.0
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -2,11 +2,13 @@
import asyncio
import logging
from collections import OrderedDict
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple, cast
import jwt
from homeassistant import data_entry_flow
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util
@@ -242,8 +244,12 @@ class AuthManager:
modules[module_id] = module.name
return modules
async def async_create_refresh_token(self, user: models.User,
client_id: Optional[str] = None) \
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
@@ -254,10 +260,36 @@ class AuthManager:
'System generated users cannot have refresh tokens connected '
'to a client.')
if not user.system_generated and client_id is None:
if token_type is None:
if user.system_generated:
token_type = models.TOKEN_TYPE_SYSTEM
else:
token_type = models.TOKEN_TYPE_NORMAL
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError(
'System generated users can only have system type '
'refresh tokens')
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
raise ValueError('Client is required to generate a refresh token.')
return await self._store.async_create_refresh_token(user, client_id)
if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and
client_name is None):
raise ValueError('Client_name is required for long-lived access '
'token')
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
for token in user.refresh_tokens.values():
if (token.client_name == client_name and token.token_type ==
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN):
# Each client_name can only have one
# long_lived_access_token type of refresh token
raise ValueError('{} already exists'.format(client_name))
return await self._store.async_create_refresh_token(
user, client_id, client_name, client_icon,
token_type, access_token_expiration)
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
@@ -277,13 +309,17 @@ class AuthManager:
@callback
def async_create_access_token(self,
refresh_token: models.RefreshToken) -> str:
refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> str:
"""Create a new access token."""
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
# pylint: disable=no-self-use
now = dt_util.utcnow()
return jwt.encode({
'iss': refresh_token.id,
'iat': dt_util.utcnow(),
'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
'iat': now,
'exp': now + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode()
async def async_validate_access_token(

View File

@@ -5,6 +5,7 @@ from logging import getLogger
from typing import Any, Dict, List, Optional # noqa: F401
import hmac
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util
@@ -128,11 +129,27 @@ class AuthStore:
self._async_schedule_save()
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None) \
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
"""Create a new token for a user."""
refresh_token = models.RefreshToken(user=user, client_id=client_id)
kwargs = {
'user': user,
'client_id': client_id,
'token_type': token_type,
'access_token_expiration': access_token_expiration
} # type: Dict[str, Any]
if client_name:
kwargs['client_name'] = client_name
if client_icon:
kwargs['client_icon'] = client_icon
refresh_token = models.RefreshToken(**kwargs)
user.refresh_tokens[refresh_token.id] = refresh_token
self._async_schedule_save()
return refresh_token
@@ -178,6 +195,15 @@ class AuthStore:
return found
@callback
def async_log_refresh_token_usage(
self, refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> None:
"""Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow()
refresh_token.last_used_ip = remote_ip
self._async_schedule_save()
async def _async_load(self) -> None:
"""Load the users."""
data = await self._store.async_load()
@@ -216,15 +242,36 @@ class AuthStore:
'Ignoring refresh token %(id)s with invalid created_at '
'%(created_at)s for user_id %(user_id)s', rt_dict)
continue
token_type = rt_dict.get('token_type')
if token_type is None:
if rt_dict['client_id'] is None:
token_type = models.TOKEN_TYPE_SYSTEM
else:
token_type = models.TOKEN_TYPE_NORMAL
# old refresh_token don't have last_used_at (pre-0.78)
last_used_at_str = rt_dict.get('last_used_at')
if last_used_at_str:
last_used_at = dt_util.parse_datetime(last_used_at_str)
else:
last_used_at = None
token = models.RefreshToken(
id=rt_dict['id'],
user=users[rt_dict['user_id']],
client_id=rt_dict['client_id'],
# use dict.get to keep backward compatibility
client_name=rt_dict.get('client_name'),
client_icon=rt_dict.get('client_icon'),
token_type=token_type,
created_at=created_at,
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
jwt_key=rt_dict['jwt_key']
jwt_key=rt_dict['jwt_key'],
last_used_at=last_used_at,
last_used_ip=rt_dict.get('last_used_ip'),
)
users[rt_dict['user_id']].refresh_tokens[token.id] = token
@@ -271,11 +318,18 @@ class AuthStore:
'id': refresh_token.id,
'user_id': user.id,
'client_id': refresh_token.client_id,
'client_name': refresh_token.client_name,
'client_icon': refresh_token.client_icon,
'token_type': refresh_token.token_type,
'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
'jwt_key': refresh_token.jwt_key,
'last_used_at':
refresh_token.last_used_at.isoformat()
if refresh_token.last_used_at else None,
'last_used_ip': refresh_token.last_used_ip,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()

View File

@@ -7,9 +7,12 @@ import attr
from homeassistant.util import dt as dt_util
from .const import ACCESS_TOKEN_EXPIRATION
from .util import generate_secret
TOKEN_TYPE_NORMAL = 'normal'
TOKEN_TYPE_SYSTEM = 'system'
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
@attr.s(slots=True)
class User:
@@ -37,23 +40,31 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User)
client_id = attr.ib(type=str) # type: Optional[str]
client_id = attr.ib(type=Optional[str])
access_token_expiration = attr.ib(type=timedelta)
client_name = attr.ib(type=Optional[str], default=None)
client_icon = attr.ib(type=Optional[str], default=None)
token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_((
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
access_token_expiration = attr.ib(type=timedelta,
default=ACCESS_TOKEN_EXPIRATION)
token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
jwt_key = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
last_used_at = attr.ib(type=Optional[datetime], default=None)
last_used_ip = attr.ib(type=Optional[str], default=None)
@attr.s(slots=True)
class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=str) # type: Optional[str]
auth_provider_id = attr.ib(type=Optional[str])
# Allow the auth provider to store data to represent their auth.
data = attr.ib(type=dict)

View File

@@ -24,7 +24,7 @@ USER_SCHEMA = vol.Schema({
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
LEGACY_USER = 'homeassistant'
LEGACY_USER_NAME = 'Legacy API password user'
class InvalidAuthError(HomeAssistantError):
@@ -52,23 +52,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Return LEGACY_USER always."""
for credential in await self.async_credentials():
if credential.data['username'] == LEGACY_USER:
return credential
"""Return credentials for this login."""
credentials = await self.async_credentials()
if credentials:
return credentials[0]
return self.async_create_credentials({
'username': LEGACY_USER
})
return self.async_create_credentials({})
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""
Set name as LEGACY_USER always.
Return info for the user.
Will be used to populate info when creating a new user.
"""
return UserMeta(name=LEGACY_USER, is_active=True)
return UserMeta(name=LEGACY_USER_NAME, is_active=True)
class LegacyLoginFlow(LoginFlow):

View File

@@ -0,0 +1,98 @@
"""
Yale Smart Alarm client for interacting with the Yale Smart Alarm System API.
For more details about this platform, please refer to the documentation at
https://www.home-assistant.io/components/alarm_control_panel.yale_smart_alarm
"""
import logging
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['yalesmartalarmclient==0.1.4']
CONF_AREA_ID = 'area_id'
DEFAULT_NAME = 'Yale Smart Alarm'
DEFAULT_AREA_ID = '1'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the alarm platform."""
name = config[CONF_NAME]
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
area_id = config[CONF_AREA_ID]
from yalesmartalarmclient.client import (
YaleSmartAlarmClient, AuthenticationError)
try:
client = YaleSmartAlarmClient(username, password, area_id)
except AuthenticationError:
_LOGGER.error("Authentication failed. Check credentials")
return
add_entities([YaleAlarmDevice(name, client)], True)
class YaleAlarmDevice(AlarmControlPanel):
"""Represent a Yale Smart Alarm."""
def __init__(self, name, client):
"""Initialize the Yale Alarm Device."""
self._name = name
self._client = client
self._state = None
from yalesmartalarmclient.client import (YALE_STATE_DISARM,
YALE_STATE_ARM_PARTIAL,
YALE_STATE_ARM_FULL)
self._state_map = {
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY
}
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Return the state of the device."""
armed_status = self._client.get_armed_status()
self._state = self._state_map.get(armed_status)
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._client.disarm()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._client.arm_partial()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._client.arm_full()

View File

@@ -61,10 +61,12 @@ def setup(hass, config):
arlo_base_station = next((
station for station in arlo.base_stations), None)
if arlo_base_station is None:
if arlo_base_station is not None:
arlo_base_station.refresh_rate = scan_interval.total_seconds()
elif not arlo.cameras:
_LOGGER.error("No Arlo camera or base station available.")
return False
arlo_base_station.refresh_rate = scan_interval.total_seconds()
hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex:

View File

@@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
REQUIREMENTS = ['asterisk_mbox==0.4.0']
REQUIREMENTS = ['asterisk_mbox==0.5.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -2,7 +2,7 @@
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Code invalide. S'il vous pla\u00eet essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte."
"invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte."
},
"step": {
"init": {

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r."
},
"step": {
"init": {
"description": "Ahhoz, hogy haszn\u00e1lhasd a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkenneld be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3ddal. Ha m\u00e9g nincsen, akkor a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t aj\u00e1nljuk.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n add meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6z\u00f6l a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edts egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.",
"title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val"
}
},
"title": "TOTP"
}
}
}

View File

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

View File

@@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Ogiltig kod, f\u00f6rs\u00f6k igen. Om du flera g\u00e5nger i rad f\u00e5r detta fel, se till att klockan i din Home Assistant \u00e4r korrekt inst\u00e4lld."
},
"step": {
"init": {
"description": "F\u00f6r att aktivera tv\u00e5faktorsautentisering som anv\u00e4nder tidsbaserade eng\u00e5ngsl\u00f6senord, skanna QR-koden med din autentiseringsapp. Om du inte har en, rekommenderar vi antingen [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n{qr_code} \n\nN\u00e4r du har skannat koden anger du den sexsiffriga koden fr\u00e5n din app f\u00f6r att verifiera inst\u00e4llningen. Om du har problem med att skanna QR-koden, g\u00f6r en manuell inst\u00e4llning med kod ** ` {code} ` **.",
"title": "St\u00e4ll in tv\u00e5faktorsautentisering med TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@@ -12,6 +12,7 @@ be in JSON as it's more readable.
Exchange the authorization code retrieved from the login flow for tokens.
{
"client_id": "https://hassbian.local:8123/",
"grant_type": "authorization_code",
"code": "411ee2f916e648d691e937ae9344681e"
}
@@ -32,6 +33,7 @@ token.
Request a new access token using a refresh token.
{
"client_id": "https://hassbian.local:8123/",
"grant_type": "refresh_token",
"refresh_token": "IJKLMNOPQRST"
}
@@ -55,6 +57,67 @@ ever been granted by that refresh token. Response code will ALWAYS be 200.
"action": "revoke"
}
# Websocket API
## Get current user
Send websocket command `auth/current_user` will return current user of the
active websocket connection.
{
"id": 10,
"type": "auth/current_user",
}
The result payload likes
{
"id": 10,
"type": "result",
"success": true,
"result": {
"id": "USER_ID",
"name": "John Doe",
"is_owner': true,
"credentials": [
{
"auth_provider_type": "homeassistant",
"auth_provider_id": null
}
],
"mfa_modules": [
{
"id": "totp",
"name": "TOTP",
"enabled": true,
}
]
}
}
## Create a long-lived access token
Send websocket command `auth/long_lived_access_token` will create
a long-lived access token for current user. Access token will not be saved in
Home Assistant. User need to record the token in secure place.
{
"id": 11,
"type": "auth/long_lived_access_token",
"client_name": "GPS Logger",
"client_icon": null,
"lifespan": 365
}
Result will be a long-lived access token:
{
"id": 11,
"type": "result",
"success": true,
"result": "ABCDEFGH"
}
"""
import logging
import uuid
@@ -63,8 +126,10 @@ from datetime import timedelta
from aiohttp import web
import voluptuous as vol
from homeassistant.auth.models import User, Credentials
from homeassistant.auth.models import User, Credentials, \
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
@@ -83,6 +148,28 @@ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CURRENT_USER,
})
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token'
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
vol.Required('lifespan'): int, # days
vol.Required('client_name'): str,
vol.Optional('client_icon'): str,
})
WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens'
SCHEMA_WS_REFRESH_TOKENS = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_REFRESH_TOKENS,
})
WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token'
SCHEMA_WS_DELETE_REFRESH_TOKEN = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN,
vol.Required('refresh_token_id'): str,
})
RESULT_TYPE_CREDENTIALS = 'credentials'
RESULT_TYPE_USER = 'user'
@@ -100,6 +187,21 @@ async def async_setup(hass, config):
WS_TYPE_CURRENT_USER, websocket_current_user,
SCHEMA_WS_CURRENT_USER
)
hass.components.websocket_api.async_register_command(
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
websocket_create_long_lived_access_token,
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN
)
hass.components.websocket_api.async_register_command(
WS_TYPE_REFRESH_TOKENS,
websocket_refresh_tokens,
SCHEMA_WS_REFRESH_TOKENS
)
hass.components.websocket_api.async_register_command(
WS_TYPE_DELETE_REFRESH_TOKEN,
websocket_delete_refresh_token,
SCHEMA_WS_DELETE_REFRESH_TOKEN
)
await login_flow.async_setup(hass, store_result)
await mfa_setup_flow.async_setup(hass)
@@ -135,10 +237,12 @@ class TokenView(HomeAssistantView):
return await self._async_handle_revoke_token(hass, data)
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(hass, data)
return await self._async_handle_auth_code(
hass, data, str(request[KEY_REAL_IP]))
if grant_type == 'refresh_token':
return await self._async_handle_refresh_token(hass, data)
return await self._async_handle_refresh_token(
hass, data, str(request[KEY_REAL_IP]))
return self.json({
'error': 'unsupported_grant_type',
@@ -163,7 +267,7 @@ class TokenView(HomeAssistantView):
await hass.auth.async_remove_refresh_token(refresh_token)
return web.Response(status=200)
async def _async_handle_auth_code(self, hass, data):
async def _async_handle_auth_code(self, hass, data, remote_addr):
"""Handle authorization code request."""
client_id = data.get('client_id')
if client_id is None or not indieauth.verify_client_id(client_id):
@@ -199,7 +303,8 @@ class TokenView(HomeAssistantView):
refresh_token = await hass.auth.async_create_refresh_token(user,
client_id)
access_token = hass.auth.async_create_access_token(refresh_token)
access_token = hass.auth.async_create_access_token(
refresh_token, remote_addr)
return self.json({
'access_token': access_token,
@@ -209,7 +314,7 @@ class TokenView(HomeAssistantView):
int(refresh_token.access_token_expiration.total_seconds()),
})
async def _async_handle_refresh_token(self, hass, data):
async def _async_handle_refresh_token(self, hass, data, remote_addr):
"""Handle authorization code request."""
client_id = data.get('client_id')
if client_id is not None and not indieauth.verify_client_id(client_id):
@@ -237,7 +342,8 @@ class TokenView(HomeAssistantView):
'error': 'invalid_request',
}, status_code=400)
access_token = hass.auth.async_create_access_token(refresh_token)
access_token = hass.auth.async_create_access_token(
refresh_token, remote_addr)
return self.json({
'access_token': access_token,
@@ -343,3 +449,68 @@ def websocket_current_user(
}))
hass.async_create_task(async_get_current_user(connection.user))
@websocket_api.ws_require_user()
@callback
def websocket_create_long_lived_access_token(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Create or a long-lived access token."""
async def async_create_long_lived_access_token(user):
"""Create or a long-lived access token."""
refresh_token = await hass.auth.async_create_refresh_token(
user,
client_name=msg['client_name'],
client_icon=msg.get('client_icon'),
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
access_token_expiration=timedelta(days=msg['lifespan']))
access_token = hass.auth.async_create_access_token(
refresh_token)
connection.send_message_outside(
websocket_api.result_message(msg['id'], access_token))
hass.async_create_task(
async_create_long_lived_access_token(connection.user))
@websocket_api.ws_require_user()
@callback
def websocket_refresh_tokens(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Return metadata of users refresh tokens."""
current_id = connection.request.get('refresh_token_id')
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{
'id': refresh.id,
'client_id': refresh.client_id,
'client_name': refresh.client_name,
'client_icon': refresh.client_icon,
'type': refresh.token_type,
'created_at': refresh.created_at,
'is_current': refresh.id == current_id,
'last_used_at': refresh.last_used_at,
'last_used_ip': refresh.last_used_ip,
} for refresh in connection.user.refresh_tokens.values()]))
@websocket_api.ws_require_user()
@callback
def websocket_delete_refresh_token(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Handle a delete refresh token request."""
async def async_delete_refresh_token(user, refresh_token_id):
"""Delete a refresh token."""
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
if refresh_token is None:
return websocket_api.error_message(
msg['id'], 'invalid_token_id', 'Received invalid token')
await hass.auth.async_remove_refresh_token(refresh_token)
connection.send_message_outside(
websocket_api.result_message(msg['id'], {}))
hass.async_create_task(
async_delete_refresh_token(connection.user, msg['refresh_token_id']))

View File

@@ -66,7 +66,7 @@ associate with an credential if "type" set to "link_user" in
"version": 1
}
"""
import aiohttp.web
from aiohttp import web
import voluptuous as vol
from homeassistant import data_entry_flow
@@ -95,11 +95,20 @@ class AuthProvidersView(HomeAssistantView):
async def get(self, request):
"""Get available auth providers."""
hass = request.app['hass']
if not hass.components.onboarding.async_is_onboarded():
return self.json_message(
message='Onboarding not finished',
status_code=400,
message_code='onboarding_required'
)
return self.json([{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in request.app['hass'].auth.auth_providers])
} for provider in hass.auth.auth_providers])
def _prepare_result_json(result):
@@ -139,7 +148,7 @@ class LoginFlowIndexView(HomeAssistantView):
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
return web.Response(status=405)
@RequestDataValidator(vol.Schema({
vol.Required('client_id'): str,

View File

@@ -158,27 +158,26 @@ def async_reload(hass):
return hass.services.async_call(DOMAIN, SERVICE_RELOAD)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_AUTOMATIONS)
yield from _async_process_config(hass, config, component)
await _async_process_config(hass, config, component)
@asyncio.coroutine
def trigger_service_handler(service_call):
async def trigger_service_handler(service_call):
"""Handle automation triggers."""
tasks = []
for entity in component.async_extract_from_service(service_call):
tasks.append(entity.async_trigger(
service_call.data.get(ATTR_VARIABLES), True))
service_call.data.get(ATTR_VARIABLES),
skip_condition=True,
context=service_call.context))
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def turn_onoff_service_handler(service_call):
async def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls."""
tasks = []
method = 'async_{}'.format(service_call.service)
@@ -186,10 +185,9 @@ def async_setup(hass, config):
tasks.append(getattr(entity, method)())
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def toggle_service_handler(service_call):
async def toggle_service_handler(service_call):
"""Handle automation toggle service calls."""
tasks = []
for entity in component.async_extract_from_service(service_call):
@@ -199,15 +197,14 @@ def async_setup(hass, config):
tasks.append(entity.async_turn_on())
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def reload_service_handler(service_call):
async def reload_service_handler(service_call):
"""Remove all automations and load new ones from config."""
conf = yield from component.async_prepare_reload()
conf = await component.async_prepare_reload()
if conf is None:
return
yield from _async_process_config(hass, conf, component)
await _async_process_config(hass, conf, component)
hass.services.async_register(
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
@@ -272,15 +269,14 @@ class AutomationEntity(ToggleEntity):
"""Return True if entity is on."""
return self._async_detach_triggers is not None
@asyncio.coroutine
def async_added_to_hass(self) -> None:
async def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state."""
if self._initial_state is not None:
enable_automation = self._initial_state
_LOGGER.debug("Automation %s initial state %s from config "
"initial_state", self.entity_id, enable_automation)
else:
state = yield from async_get_last_state(self.hass, self.entity_id)
state = await async_get_last_state(self.hass, self.entity_id)
if state:
enable_automation = state.state == STATE_ON
self._last_triggered = state.attributes.get('last_triggered')
@@ -298,54 +294,50 @@ class AutomationEntity(ToggleEntity):
# HomeAssistant is starting up
if self.hass.state == CoreState.not_running:
@asyncio.coroutine
def async_enable_automation(event):
async def async_enable_automation(event):
"""Start automation on startup."""
yield from self.async_enable()
await self.async_enable()
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, async_enable_automation)
# HomeAssistant is running
else:
yield from self.async_enable()
await self.async_enable()
@asyncio.coroutine
def async_turn_on(self, **kwargs) -> None:
async def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on and update the state."""
if self.is_on:
return
yield from self.async_enable()
await self.async_enable()
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
async def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
if not self.is_on:
return
self._async_detach_triggers()
self._async_detach_triggers = None
yield from self.async_update_ha_state()
await self.async_update_ha_state()
@asyncio.coroutine
def async_trigger(self, variables, skip_condition=False):
async def async_trigger(self, variables, skip_condition=False,
context=None):
"""Trigger automation.
This method is a coroutine.
"""
if skip_condition or self._cond_func(variables):
yield from self._async_action(self.entity_id, variables)
self.async_set_context(context)
await self._async_action(self.entity_id, variables, context)
self._last_triggered = utcnow()
yield from self.async_update_ha_state()
await self.async_update_ha_state()
@asyncio.coroutine
def async_will_remove_from_hass(self):
async def async_will_remove_from_hass(self):
"""Remove listeners when removing automation from HASS."""
yield from self.async_turn_off()
await self.async_turn_off()
@asyncio.coroutine
def async_enable(self):
async def async_enable(self):
"""Enable this automation entity.
This method is a coroutine.
@@ -353,9 +345,9 @@ class AutomationEntity(ToggleEntity):
if self.is_on:
return
self._async_detach_triggers = yield from self._async_attach_triggers(
self._async_detach_triggers = await self._async_attach_triggers(
self.async_trigger)
yield from self.async_update_ha_state()
await self.async_update_ha_state()
@property
def device_state_attributes(self):
@@ -368,8 +360,7 @@ class AutomationEntity(ToggleEntity):
}
@asyncio.coroutine
def _async_process_config(hass, config, component):
async def _async_process_config(hass, config, component):
"""Process config and add automations.
This method is a coroutine.
@@ -411,20 +402,19 @@ def _async_process_config(hass, config, component):
entities.append(entity)
if entities:
yield from component.async_add_entities(entities)
await component.async_add_entities(entities)
def _async_get_action(hass, config, name):
"""Return an action based on a configuration."""
script_obj = script.Script(hass, config, name)
@asyncio.coroutine
def action(entity_id, variables):
async def action(entity_id, variables, context):
"""Execute an action."""
_LOGGER.info('Executing %s', name)
logbook.async_log_entry(
hass, name, 'has been triggered', DOMAIN, entity_id)
yield from script_obj.async_run(variables)
await script_obj.async_run(variables, context)
return action
@@ -448,8 +438,7 @@ def _async_process_if(hass, config, p_config):
return if_action
@asyncio.coroutine
def _async_process_trigger(hass, config, trigger_configs, name, action):
async def _async_process_trigger(hass, config, trigger_configs, name, action):
"""Set up the triggers.
This method is a coroutine.
@@ -457,13 +446,13 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
removes = []
for conf in trigger_configs:
platform = yield from async_prepare_setup_platform(
platform = await async_prepare_setup_platform(
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
if platform is None:
return None
remove = yield from platform.async_trigger(hass, conf, action)
remove = await platform.async_trigger(hass, conf, action)
if not remove:
_LOGGER.error("Error setting up trigger %s", name)

View File

@@ -45,11 +45,11 @@ def async_trigger(hass, config, action):
# If event data doesn't match requested schema, skip event
return
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'event',
'event': event,
},
})
}, context=event.context))
return hass.bus.async_listen(event_type, handle_event)

View File

@@ -32,12 +32,12 @@ def async_trigger(hass, config, action):
@callback
def hass_shutdown(event):
"""Execute when Home Assistant is shutting down."""
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'homeassistant',
'event': event,
},
})
}, context=event.context))
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
hass_shutdown)
@@ -45,11 +45,11 @@ def async_trigger(hass, config, action):
# Automation are enabled while hass is starting up, fire right away
# Check state because a config reload shouldn't trigger it.
if hass.state == CoreState.starting:
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'homeassistant',
'event': event,
},
})
}))
return lambda: None

View File

@@ -66,7 +66,7 @@ def async_trigger(hass, config, action):
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'numeric_state',
'entity_id': entity,
@@ -75,7 +75,7 @@ def async_trigger(hass, config, action):
'from_state': from_s,
'to_state': to_s,
}
})
}, context=to_s.context))
matching = check_numeric_state(entity, from_s, to_s)

View File

@@ -43,7 +43,7 @@ def async_trigger(hass, config, action):
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'state',
'entity_id': entity,
@@ -51,7 +51,7 @@ def async_trigger(hass, config, action):
'to_state': to_s,
'for': time_delta,
}
})
}, context=to_s.context))
# Ignore changes to state attributes if from/to is in use
if (not match_all and from_s is not None and to_s is not None and

View File

@@ -32,13 +32,13 @@ def async_trigger(hass, config, action):
@callback
def template_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action."""
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'template',
'entity_id': entity_id,
'from_state': from_s,
'to_state': to_s,
},
})
}, context=to_s.context))
return async_track_template(hass, value_template, template_listener)

View File

@@ -51,7 +51,7 @@ def async_trigger(hass, config, action):
# pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \
event == EVENT_LEAVE and from_match and not to_match:
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'zone',
'entity_id': entity,
@@ -60,7 +60,7 @@ def async_trigger(hass, config, action):
'zone': zone_state,
'event': event,
},
})
}, context=to_s.context))
return async_track_state_change(hass, entity_id, zone_automation_listener,
MATCH_ALL, MATCH_ALL)

View File

@@ -54,6 +54,11 @@ class DeconzBinarySensor(BinarySensorDevice):
self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed."""
self._sensor.remove_callback(self.async_update_callback)
self._sensor = None
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.

View File

@@ -7,12 +7,11 @@ https://home-assistant.io/components/binary_sensor.openuv/
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.openuv import (
BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE,
TYPE_PROTECTION_WINDOW, OpenUvEntity)
BINARY_SENSORS, DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN,
TOPIC_UPDATE, TYPE_PROTECTION_WINDOW, OpenUvEntity)
from homeassistant.util.dt import as_local, parse_datetime, utcnow
DEPENDENCIES = ['openuv']
@@ -26,17 +25,20 @@ ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the OpenUV binary sensor platform."""
if discovery_info is None:
return
"""Set up an OpenUV sensor based on existing config."""
pass
openuv = hass.data[DOMAIN]
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up an OpenUV sensor based on a config entry."""
openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id]
binary_sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
for sensor_type in openuv.binary_sensor_conditions:
name, icon = BINARY_SENSORS[sensor_type]
binary_sensors.append(
OpenUvBinarySensor(openuv, sensor_type, name, icon))
OpenUvBinarySensor(
openuv, sensor_type, name, icon, entry.entry_id))
async_add_entities(binary_sensors, True)
@@ -44,14 +46,16 @@ async def async_setup_platform(
class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
"""Define a binary sensor for OpenUV."""
def __init__(self, openuv, sensor_type, name, icon):
def __init__(self, openuv, sensor_type, name, icon, entry_id):
"""Initialize the sensor."""
super().__init__(openuv)
self._entry_id = entry_id
self._icon = icon
self._latitude = openuv.client.latitude
self._longitude = openuv.client.longitude
self._name = name
self._dispatch_remove = None
self._sensor_type = sensor_type
self._state = None
@@ -83,8 +87,9 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self._dispatch_remove = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, self._update_data)
self.async_on_remove(self._dispatch_remove)
async def async_update(self):
"""Update the state."""

View File

@@ -142,6 +142,68 @@ def async_snapshot(hass, filename, entity_id=None):
@bind_hass
async def async_get_image(hass, entity_id, timeout=10):
"""Fetch an image from a camera entity."""
camera = _get_camera_from_entity_id(hass, entity_id)
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop):
image = await camera.async_camera_image()
if image:
return Image(camera.content_type, image)
raise HomeAssistantError('Unable to get image')
@bind_hass
async def async_get_mjpeg_stream(hass, request, entity_id):
"""Fetch an mjpeg stream from a camera entity."""
camera = _get_camera_from_entity_id(hass, entity_id)
return await camera.handle_async_mjpeg_stream(request)
async def async_get_still_stream(request, image_cb, content_type, interval):
"""Generate an HTTP MJPEG stream from camera images.
This method must be run in the event loop.
"""
response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary')
await response.prepare(request)
async def write_to_mjpeg_stream(img_bytes):
"""Write image to stream."""
await response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
last_image = None
while True:
img_bytes = await image_cb()
if not img_bytes:
break
if img_bytes != last_image:
await write_to_mjpeg_stream(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes
await asyncio.sleep(interval)
return response
def _get_camera_from_entity_id(hass, entity_id):
"""Get camera component from entity_id."""
component = hass.data.get(DOMAIN)
if component is None:
@@ -155,14 +217,7 @@ async def async_get_image(hass, entity_id, timeout=10):
if not camera.is_on:
raise HomeAssistantError('Camera is off')
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop):
image = await camera.async_camera_image()
if image:
return Image(camera.content_type, image)
raise HomeAssistantError('Unable to get image')
return camera
async def async_setup(hass, config):
@@ -290,39 +345,8 @@ class Camera(Entity):
This method must be run in the event loop.
"""
response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary')
await response.prepare(request)
async def write_to_mjpeg_stream(img_bytes):
"""Write image to stream."""
await response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
self.content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
last_image = None
while True:
img_bytes = await self.async_camera_image()
if not img_bytes:
break
if img_bytes and img_bytes != last_image:
await write_to_mjpeg_stream(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes
await asyncio.sleep(interval)
return response
return await async_get_still_stream(request, self.async_camera_image,
self.content_type, interval)
async def handle_async_mjpeg_stream(self, request):
"""Serve an HTTP MJPEG stream from the camera.

View File

@@ -7,17 +7,15 @@ https://www.home-assistant.io/components/camera.proxy/
import asyncio
import logging
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_web, async_get_clientsession)
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from . import async_get_still_stream
REQUIREMENTS = ['pillow==5.2.0']
@@ -158,22 +156,14 @@ class ProxyCamera(Camera):
return self._last_image
self._last_image_time = now
url = "{}/api/camera_proxy/{}".format(
self.hass.config.api.base_url, self._proxied_camera)
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop):
response = await websession.get(url, headers=self._headers)
image = await response.read()
except asyncio.TimeoutError:
_LOGGER.error("Timeout getting camera image")
return self._last_image
except aiohttp.ClientError as err:
_LOGGER.error("Error getting new camera image: %s", err)
image = await self.hass.components.camera.async_get_image(
self._proxied_camera)
if not image:
_LOGGER.error("Error getting original camera image")
return self._last_image
image = await self.hass.async_add_job(
_resize_image, image, self._image_opts)
_resize_image, image.content, self._image_opts)
if self._cache_images:
self._last_image = image
@@ -181,56 +171,28 @@ class ProxyCamera(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images."""
websession = async_get_clientsession(self.hass)
url = "{}/api/camera_proxy_stream/{}".format(
self.hass.config.api.base_url, self._proxied_camera)
stream_coro = websession.get(url, headers=self._headers)
if not self._stream_opts:
return await async_aiohttp_proxy_web(
self.hass, request, stream_coro)
return await self.hass.components.camera.async_get_mjpeg_stream(
request, self._proxied_camera)
response = aiohttp.web.StreamResponse()
response.content_type = (
'multipart/x-mixed-replace; boundary=--frameboundary')
await response.prepare(request)
async def write(img_bytes):
"""Write image to stream."""
await response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
self.content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
with async_timeout.timeout(10, loop=self.hass.loop):
req = await stream_coro
try:
# This would be nicer as an async generator
# But that would only be supported for python >=3.6
data = b''
stream = req.content
while True:
chunk = await stream.read(102400)
if not chunk:
break
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
image = data[jpg_start:jpg_end + 2]
image = await self.hass.async_add_job(
_resize_image, image, self._stream_opts)
await write(image)
data = data[jpg_end + 2:]
finally:
req.close()
return response
return await async_get_still_stream(
request, self._async_stream_image,
self.content_type, self.frame_interval)
@property
def name(self):
"""Return the name of this camera."""
return self._name
async def _async_stream_image(self):
"""Return a still image response from the camera."""
try:
image = await self.hass.components.camera.async_get_image(
self._proxied_camera)
if not image:
return None
except HomeAssistantError:
raise asyncio.CancelledError
return await self.hass.async_add_job(
_resize_image, image.content, self._stream_opts)

View File

@@ -13,8 +13,10 @@ import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING
from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST
from homeassistant.components.http.view import KEY_AUTHENTICATED,\
HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\
HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util
@@ -25,11 +27,13 @@ DEPENDENCIES = ['http']
CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field'
CONF_TOKEN = 'token'
DEFAULT_NAME = "Push Camera"
ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip'
ATTR_TOKEN = 'token'
PUSH_CAMERA_DATA = 'push_camera'
@@ -39,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string,
vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)),
})
@@ -50,7 +55,8 @@ async def async_setup_platform(hass, config, async_add_entities,
cameras = [PushCamera(config[CONF_NAME],
config[CONF_BUFFER_SIZE],
config[CONF_TIMEOUT])]
config[CONF_TIMEOUT],
config.get(CONF_TOKEN))]
hass.http.register_view(CameraPushReceiver(hass,
config[CONF_IMAGE_FIELD]))
@@ -63,6 +69,7 @@ class CameraPushReceiver(HomeAssistantView):
url = "/api/camera_push/{entity_id}"
name = 'api:camera_push:camera_entity'
requires_auth = False
def __init__(self, hass, image_field):
"""Initialize CameraPushReceiver with camera entity."""
@@ -75,8 +82,21 @@ class CameraPushReceiver(HomeAssistantView):
if _camera is None:
_LOGGER.error("Unknown %s", entity_id)
status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\
else HTTP_UNAUTHORIZED
return self.json_message('Unknown {}'.format(entity_id),
HTTP_BAD_REQUEST)
status)
# Supports HA authentication and token based
# when token has been configured
authenticated = (request[KEY_AUTHENTICATED] or
(_camera.token is not None and
request.query.get('token') == _camera.token))
if not authenticated:
return self.json_message(
'Invalid authorization credentials for {}'.format(entity_id),
HTTP_UNAUTHORIZED)
try:
data = await request.post()
@@ -95,7 +115,7 @@ class CameraPushReceiver(HomeAssistantView):
class PushCamera(Camera):
"""The representation of a Push camera."""
def __init__(self, name, buffer_size, timeout):
def __init__(self, name, buffer_size, timeout, token):
"""Initialize push camera component."""
super().__init__()
self._name = name
@@ -106,6 +126,7 @@ class PushCamera(Camera):
self._timeout = timeout
self.queue = deque([], buffer_size)
self._current_image = None
self.token = token
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
@@ -168,5 +189,6 @@ class PushCamera(Camera):
name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename),
(ATTR_TOKEN, self.token),
) if value is not None
}

View File

@@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.",
"single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Google Cast."
"single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Google Cast."
},
"step": {
"confirm": {

View File

@@ -2,12 +2,14 @@
"config": {
"abort": {
"no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.",
"single_instance_allowed": "Seulement une seule configuration de Google Cast est n\u00e9cessaire."
"single_instance_allowed": "Une seule configuration de Google Cast est n\u00e9cessaire."
},
"step": {
"confirm": {
"description": "Voulez-vous configurer Google Cast?"
"description": "Voulez-vous configurer Google Cast?",
"title": "Google Cast"
}
}
},
"title": "Google Cast"
}
}

View File

@@ -251,6 +251,14 @@ class GenericThermostat(ClimateDevice):
# Ensure we update the current operation after changing the mode
self.schedule_update_ha_state()
async def async_turn_on(self):
"""Turn thermostat on."""
await self.async_set_operation_mode(self.operation_list[0])
async def async_turn_off(self):
"""Turn thermostat off."""
await self.async_set_operation_mode(STATE_OFF)
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)

View File

@@ -8,7 +8,8 @@ import logging
import voluptuous as vol
from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE
from homeassistant.components.nest import (
DATA_NEST, SIGNAL_NEST_UPDATE, DOMAIN as NEST_DOMAIN)
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice,
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
@@ -127,6 +128,19 @@ class NestThermostat(ClimateDevice):
"""Return unique ID for this device."""
return self.device.serial
@property
def device_info(self):
"""Return information about the device."""
return {
'identifiers': {
(NEST_DOMAIN, self.device.device_id),
},
'name': self.device.name_long,
'manufacturer': 'Nest Labs',
'model': "Thermostat",
'sw_version': self.device.software_version,
}
@property
def name(self):
"""Return the name of the nest, if any."""

View File

@@ -0,0 +1,189 @@
"""
Support for OpenTherm Gateway devices.
For more details about this component, please refer to the documentation at
http://home-assistant.io/components/climate.opentherm_gw/
"""
import logging
import voluptuous as vol
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA,
STATE_IDLE, STATE_HEAT,
STATE_COOL,
SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME,
PRECISION_HALVES, PRECISION_TENTHS,
TEMP_CELSIUS, PRECISION_WHOLE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyotgw==0.1b0']
CONF_FLOOR_TEMP = "floor_temperature"
CONF_PRECISION = 'precision'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICE): cv.string,
vol.Optional(CONF_NAME, default="OpenTherm Gateway"): cv.string,
vol.Optional(CONF_PRECISION): vol.In([PRECISION_TENTHS, PRECISION_HALVES,
PRECISION_WHOLE]),
vol.Optional(CONF_FLOOR_TEMP, default=False): cv.boolean,
})
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the opentherm_gw device."""
gateway = OpenThermGateway(config)
async_add_entities([gateway])
class OpenThermGateway(ClimateDevice):
"""Representation of a climate device."""
def __init__(self, config):
"""Initialize the sensor."""
import pyotgw
self.pyotgw = pyotgw
self.gateway = self.pyotgw.pyotgw()
self._device = config[CONF_DEVICE]
self.friendly_name = config.get(CONF_NAME)
self.floor_temp = config.get(CONF_FLOOR_TEMP)
self.temp_precision = config.get(CONF_PRECISION)
self._current_operation = STATE_IDLE
self._current_temperature = 0.0
self._target_temperature = 0.0
self._away_mode_a = None
self._away_mode_b = None
self._away_state_a = False
self._away_state_b = False
async def async_added_to_hass(self):
"""Connect to the OpenTherm Gateway device."""
await self.gateway.connect(self.hass.loop, self._device)
self.gateway.subscribe(self.receive_report)
_LOGGER.debug("Connected to %s on %s", self.friendly_name,
self._device)
async def receive_report(self, status):
"""Receive and handle a new report from the Gateway."""
_LOGGER.debug("Received report: %s", status)
ch_active = status.get(self.pyotgw.DATA_SLAVE_CH_ACTIVE)
cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE)
if ch_active:
self._current_operation = STATE_HEAT
elif cooling_active:
self._current_operation = STATE_COOL
else:
self._current_operation = STATE_IDLE
self._current_temperature = status.get(self.pyotgw.DATA_ROOM_TEMP)
temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT_OVRD)
if temp is None:
temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT)
self._target_temperature = temp
# GPIO mode 5: 0 == Away
# GPIO mode 6: 1 == Away
gpio_a_state = status.get(self.pyotgw.OTGW_GPIO_A)
if gpio_a_state == 5:
self._away_mode_a = 0
elif gpio_a_state == 6:
self._away_mode_a = 1
else:
self._away_mode_a = None
gpio_b_state = status.get(self.pyotgw.OTGW_GPIO_B)
if gpio_b_state == 5:
self._away_mode_b = 0
elif gpio_b_state == 6:
self._away_mode_b = 1
else:
self._away_mode_b = None
if self._away_mode_a is not None:
self._away_state_a = (status.get(self.pyotgw.OTGW_GPIO_A_STATE) ==
self._away_mode_a)
if self._away_mode_b is not None:
self._away_state_b = (status.get(self.pyotgw.OTGW_GPIO_B_STATE) ==
self._away_mode_b)
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the friendly name."""
return self.friendly_name
@property
def precision(self):
"""Return the precision of the system."""
if self.temp_precision is not None:
return self.temp_precision
if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
return PRECISION_HALVES
return PRECISION_WHOLE
@property
def should_poll(self):
"""Disable polling for this entity."""
return False
@property
def temperature_unit(self):
"""Return the unit of measurement used by the platform."""
return TEMP_CELSIUS
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_operation
@property
def current_temperature(self):
"""Return the current temperature."""
if self.floor_temp is True:
if self.temp_precision == PRECISION_HALVES:
return int(2 * self._current_temperature) / 2
if self.temp_precision == PRECISION_TENTHS:
return int(10 * self._current_temperature) / 10
return int(self._current_temperature)
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return self.temp_precision
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away_state_a or self._away_state_b
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs:
temp = float(kwargs[ATTR_TEMPERATURE])
self._target_temperature = await self.gateway.set_target_temp(
temp)
self.async_schedule_update_ha_state()
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def min_temp(self):
"""Return the minimum temperature."""
return 1
@property
def max_temp(self):
"""Return the maximum temperature."""
return 30

View File

@@ -174,8 +174,8 @@ class RadioThermostat(ClimateDevice):
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
ATTR_FAN: self._fmode,
ATTR_MODE: self._tmode,
ATTR_FAN: self._fstate,
ATTR_MODE: self._tstate,
}
@property

View File

@@ -7,9 +7,6 @@ from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
REQUIREMENTS = ['voluptuous-serialize==2.0.0']
@asyncio.coroutine
def async_setup(hass):
"""Enable the Home Assistant views."""

View File

@@ -0,0 +1,73 @@
"""
Support for Insteon covers via PowerLinc Modem.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/cover.insteon/
"""
import logging
import math
from homeassistant.components.insteon import InsteonEntity
from homeassistant.components.cover import (CoverDevice, ATTR_POSITION,
SUPPORT_OPEN, SUPPORT_CLOSE,
SUPPORT_SET_POSITION)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['insteon']
SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Insteon platform."""
if not discovery_info:
return
insteon_modem = hass.data['insteon'].get('modem')
address = discovery_info['address']
device = insteon_modem.devices[address]
state_key = discovery_info['state_key']
_LOGGER.debug('Adding device %s entity %s to Cover platform',
device.address.hex, device.states[state_key].name)
new_entity = InsteonCoverDevice(device, state_key)
async_add_entities([new_entity])
class InsteonCoverDevice(InsteonEntity, CoverDevice):
"""A Class for an Insteon device."""
@property
def current_cover_position(self):
"""Return the current cover position."""
return int(math.ceil(self._insteon_device_state.value*100/255))
@property
def supported_features(self):
"""Return the supported features for this entity."""
return SUPPORTED_FEATURES
@property
def is_closed(self):
"""Return the boolean response if the node is on."""
return bool(self.current_cover_position)
async def async_open_cover(self, **kwargs):
"""Open device."""
self._insteon_device_state.open()
async def async_close_cover(self, **kwargs):
"""Close device."""
self._insteon_device_state.close()
async def async_set_cover_position(self, **kwargs):
"""Set the cover position."""
position = int(kwargs[ATTR_POSITION]*255/100)
if position == 0:
self._insteon_device_state.close()
else:
self._insteon_device_state.set_position(position)

View File

@@ -8,17 +8,25 @@ import logging
import voluptuous as vol
from homeassistant.components.cover import CoverDevice
from homeassistant.components.cover import (
CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING,
STATE_OPENING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymyq==0.0.11']
REQUIREMENTS = ['pymyq==0.0.15']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'myq'
MYQ_TO_HASS = {
'closed': STATE_CLOSED,
'closing': STATE_CLOSING,
'opening': STATE_OPENING
}
NOTIFICATION_ID = 'myq_notification'
NOTIFICATION_TITLE = 'MyQ Cover Setup'
@@ -87,7 +95,17 @@ class MyQDevice(CoverDevice):
@property
def is_closed(self):
"""Return true if cover is closed, else False."""
return self._status == STATE_CLOSED
return MYQ_TO_HASS[self._status] == STATE_CLOSED
@property
def is_closing(self):
"""Return if the cover is closing or not."""
return MYQ_TO_HASS[self._status] == STATE_CLOSING
@property
def is_opening(self):
"""Return if the cover is opening or not."""
return MYQ_TO_HASS[self._status] == STATE_OPENING
def close_cover(self, **kwargs):
"""Issue close command to cover."""
@@ -97,6 +115,16 @@ class MyQDevice(CoverDevice):
"""Issue open command to cover."""
self.myq.open_device(self.device_id)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE
@property
def unique_id(self):
"""Return a unique, HASS-friendly identifier for this entity."""
return self.device_id
def update(self):
"""Update status of cover."""
self._status = self.myq.get_status(self.device_id)

View File

@@ -92,9 +92,9 @@ class RflinkCover(RflinkCommand, CoverDevice):
self.cancel_queued_send_commands()
command = event['command']
if command in ['on', 'allon']:
if command in ['on', 'allon', 'up']:
self._state = True
elif command in ['off', 'alloff']:
elif command in ['off', 'alloff', 'down']:
self._state = False
@property
@@ -105,7 +105,12 @@ class RflinkCover(RflinkCommand, CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed."""
return None
return not self._state
@property
def assumed_state(self):
"""Return True because covers can be stopped midway."""
return True
def async_close_cover(self, **kwargs):
"""Turn the device close."""

View File

@@ -22,7 +22,8 @@
},
"options": {
"data": {
"allow_clip_sensor": "Autoriser l'importation de capteurs virtuels"
"allow_clip_sensor": "Autoriser l'importation de capteurs virtuels",
"allow_deconz_groups": "Autoriser l'importation des groupes deCONZ"
},
"title": "Options de configuration suppl\u00e9mentaires pour deCONZ"
}

View File

@@ -28,6 +28,6 @@
"title": "Ekstra konfigurasjonsalternativer for deCONZ"
}
},
"title": "deCONZ"
"title": "deCONZ Zigbee gateway"
}
}

View File

@@ -24,7 +24,7 @@ from .const import (
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==44']
REQUIREMENTS = ['pydeconz==47']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -46,6 +46,8 @@ SERVICE_SCHEMA = vol.Schema({
vol.Required(SERVICE_DATA): dict,
})
SERVICE_DEVICE_REFRESH = 'device_refresh'
async def async_setup(hass, config):
"""Load configuration for deCONZ component.
@@ -84,15 +86,17 @@ async def async_setup_entry(hass, config_entry):
@callback
def async_add_device_callback(device_type, device):
"""Handle event of new device creation in deCONZ."""
if not isinstance(device, list):
device = [device]
async_dispatcher_send(
hass, 'deconz_new_{}'.format(device_type), [device])
hass, 'deconz_new_{}'.format(device_type), device)
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **config_entry.data,
async_add_device=async_add_device_callback)
result = await deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ")
return False
hass.data[DOMAIN] = deconz
@@ -149,16 +153,60 @@ async def async_setup_entry(hass, config_entry):
data = call.data.get(SERVICE_DATA)
deconz = hass.data[DOMAIN]
if entity_id:
entities = hass.data.get(DATA_DECONZ_ID)
if entities:
field = entities.get(entity_id)
if field is None:
_LOGGER.error('Could not find the entity %s', entity_id)
return
await deconz.async_put_state(field, data)
hass.services.async_register(
DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA)
async def async_refresh_devices(call):
"""Refresh available devices from deCONZ."""
deconz = hass.data[DOMAIN]
groups = list(deconz.groups.keys())
lights = list(deconz.lights.keys())
scenes = list(deconz.scenes.keys())
sensors = list(deconz.sensors.keys())
if not await deconz.async_load_parameters():
return
async_add_device_callback(
'group', [group
for group_id, group in deconz.groups.items()
if group_id not in groups]
)
async_add_device_callback(
'light', [light
for light_id, light in deconz.lights.items()
if light_id not in lights]
)
async_add_device_callback(
'scene', [scene
for scene_id, scene in deconz.scenes.items()
if scene_id not in scenes]
)
async_add_device_callback(
'sensor', [sensor
for sensor_id, sensor in deconz.sensors.items()
if sensor_id not in sensors]
)
hass.services.async_register(
DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices)
@callback
def deconz_shutdown(event):
"""
@@ -179,15 +227,22 @@ async def async_unload_entry(hass, config_entry):
deconz = hass.data.pop(DOMAIN)
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
deconz.close()
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
await hass.config_entries.async_forward_entry_unload(
config_entry, component)
dispatchers = hass.data[DATA_DECONZ_UNSUB]
for unsub_dispatcher in dispatchers:
unsub_dispatcher()
hass.data[DATA_DECONZ_UNSUB] = []
hass.data[DATA_DECONZ_EVENT] = []
for event in hass.data[DATA_DECONZ_EVENT]:
event.async_will_remove_from_hass()
hass.data[DATA_DECONZ_EVENT].remove(event)
hass.data[DATA_DECONZ_ID] = []
return True
@@ -206,6 +261,12 @@ class DeconzEvent:
self._event = 'deconz_{}'.format(CONF_EVENT)
self._id = slugify(self._device.name)
@callback
def async_will_remove_from_hass(self) -> None:
"""Disconnect event object when removed."""
self._device.remove_callback(self.async_update_callback)
self._device = None
@callback
def async_update_callback(self, reason):
"""Fire the event if reason is that state is updated."""

View File

@@ -1,4 +1,3 @@
configure:
description: Set attribute of device in deCONZ. See https://home-assistant.io/components/deconz/#device-services for details.
fields:
@@ -11,3 +10,6 @@ configure:
data:
description: Data is a json object with what data you want to alter.
example: '{"on": true}'
device_refresh:
description: Refresh device lists from deCONZ.

View File

@@ -6,35 +6,25 @@ https://home-assistant.io/components/device_tracker.bluetooth_le_tracker/
"""
import logging
import voluptuous as vol
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
PLATFORM_SCHEMA, load_config, SOURCE_TYPE_BLUETOOTH_LE
load_config, SOURCE_TYPE_BLUETOOTH_LE
)
import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['gattlib==0.20150805']
REQUIREMENTS = ['pygatt==3.2.0']
BLE_PREFIX = 'BLE_'
MIN_SEEN_NEW = 5
CONF_SCAN_DURATION = 'scan_duration'
CONF_BLUETOOTH_DEVICE = 'device_id'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_DURATION, default=10): cv.positive_int,
vol.Optional(CONF_BLUETOOTH_DEVICE, default='hci0'): cv.string
})
def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Bluetooth LE Scanner."""
# pylint: disable=import-error
from gattlib import DiscoveryService
import pygatt
new_devices = {}
def see_device(address, name, new_device=False):
@@ -61,17 +51,17 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Discover Bluetooth LE devices."""
_LOGGER.debug("Discovering Bluetooth LE devices")
try:
service = DiscoveryService(ble_dev_id)
devices = service.discover(duration)
adapter = pygatt.GATTToolBackend()
devs = adapter.scan()
devices = {x['address']: x['name'] for x in devs}
_LOGGER.debug("Bluetooth LE devices discovered = %s", devices)
except RuntimeError as error:
_LOGGER.error("Error during Bluetooth LE scan: %s", error)
devices = []
return {}
return devices
yaml_path = hass.config.path(YAML_DEVICES)
duration = config.get(CONF_SCAN_DURATION)
ble_dev_id = config.get(CONF_BLUETOOTH_DEVICE)
devs_to_track = []
devs_donot_track = []
@@ -102,11 +92,11 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Lookup Bluetooth LE devices and update status."""
devs = discover_ble_devices()
for mac in devs_to_track:
_LOGGER.debug("Checking %s", mac)
result = mac in devs
if not result:
# Could not lookup device name
if mac not in devs:
continue
if devs[mac] is None:
devs[mac] = mac
see_device(mac, devs[mac])
if track_new:
@@ -119,5 +109,4 @@ def setup_scanner(hass, config, see, discovery_info=None):
track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval)
update_ble(dt_util.utcnow())
return True

View File

@@ -12,7 +12,8 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH)
load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH,
DOMAIN)
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -79,7 +80,13 @@ def setup_scanner(hass, config, see, discovery_info=None):
request_rssi = config.get(CONF_REQUEST_RSSI, False)
def update_bluetooth(now):
def update_bluetooth(_):
"""Update Bluetooth and set timer for the next update."""
update_bluetooth_once()
track_point_in_utc_time(
hass, update_bluetooth, dt_util.utcnow() + interval)
def update_bluetooth_once():
"""Lookup Bluetooth device and update status."""
try:
if track_new:
@@ -99,9 +106,14 @@ def setup_scanner(hass, config, see, discovery_info=None):
see_device(mac, result, rssi)
except bluetooth.BluetoothError:
_LOGGER.exception("Error looking up Bluetooth device")
track_point_in_utc_time(
hass, update_bluetooth, dt_util.utcnow() + interval)
def handle_update_bluetooth(call):
"""Update bluetooth devices on demand."""
update_bluetooth_once()
update_bluetooth(dt_util.utcnow())
hass.services.register(
DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth)
return True

View File

@@ -15,7 +15,7 @@ from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from homeassistant.util import slugify, dt as dt_util
REQUIREMENTS = ['locationsharinglib==2.0.11']
@@ -92,7 +92,7 @@ class GoogleMapsScanner:
ATTR_ADDRESS: person.address,
ATTR_FULL_NAME: person.full_name,
ATTR_ID: person.id,
ATTR_LAST_SEEN: person.datetime,
ATTR_LAST_SEEN: dt_util.as_utc(person.datetime),
ATTR_NICKNAME: person.nickname,
}
self.see(

View File

@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==2.0.0']
REQUIREMENTS = ['netdisco==2.1.0']
DOMAIN = 'discovery'

View File

@@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180831.0']
REQUIREMENTS = ['home-assistant-frontend==20180916.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',

View File

@@ -0,0 +1,68 @@
"""
Geo Location component.
This component covers platforms that deal with external events that contain
a geo location related to the installed HA instance.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/geo_location/
"""
import logging
from datetime import timedelta
from typing import Optional
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
_LOGGER = logging.getLogger(__name__)
ATTR_DISTANCE = 'distance'
DOMAIN = 'geo_location'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
GROUP_NAME_ALL_EVENTS = 'All Geo Location Events'
SCAN_INTERVAL = timedelta(seconds=60)
async def async_setup(hass, config):
"""Set up this component."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS)
await component.async_setup(config)
return True
class GeoLocationEvent(Entity):
"""This represents an external event with an associated geo location."""
@property
def state(self):
"""Return the state of the sensor."""
if self.distance is not None:
return round(self.distance, 1)
return None
@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return None
@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return None
@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return None
@property
def state_attributes(self):
"""Return the state attributes of this external event."""
data = {}
if self.latitude is not None:
data[ATTR_LATITUDE] = round(self.latitude, 5)
if self.longitude is not None:
data[ATTR_LONGITUDE] = round(self.longitude, 5)
return data

View File

@@ -0,0 +1,132 @@
"""
Demo platform for the geo location component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import logging
import random
from datetime import timedelta
from math import pi, cos, sin, radians
from typing import Optional
from homeassistant.components.geo_location import GeoLocationEvent
from homeassistant.helpers.event import track_time_interval
_LOGGER = logging.getLogger(__name__)
AVG_KM_PER_DEGREE = 111.0
DEFAULT_UNIT_OF_MEASUREMENT = "km"
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
MAX_RADIUS_IN_KM = 50
NUMBER_OF_DEMO_DEVICES = 5
EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off",
"Structure Fire", "Fire Alarm", "Thunderstorm", "Tornado",
"Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm",
"Earthquake", "Tsunami"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo geo locations."""
DemoManager(hass, add_entities)
class DemoManager:
"""Device manager for demo geo location events."""
def __init__(self, hass, add_entities):
"""Initialise the demo geo location event manager."""
self._hass = hass
self._add_entities = add_entities
self._managed_devices = []
self._update(count=NUMBER_OF_DEMO_DEVICES)
self._init_regular_updates()
def _generate_random_event(self):
"""Generate a random event in vicinity of this HA instance."""
home_latitude = self._hass.config.latitude
home_longitude = self._hass.config.longitude
# Approx. 111km per degree (north-south).
radius_in_degrees = random.random() * MAX_RADIUS_IN_KM / \
AVG_KM_PER_DEGREE
radius_in_km = radius_in_degrees * AVG_KM_PER_DEGREE
angle = random.random() * 2 * pi
# Compute coordinates based on radius and angle. Adjust longitude value
# based on HA's latitude.
latitude = home_latitude + radius_in_degrees * sin(angle)
longitude = home_longitude + radius_in_degrees * cos(angle) / \
cos(radians(home_latitude))
event_name = random.choice(EVENT_NAMES)
return DemoGeoLocationEvent(event_name, radius_in_km, latitude,
longitude, DEFAULT_UNIT_OF_MEASUREMENT)
def _init_regular_updates(self):
"""Schedule regular updates based on configured time interval."""
track_time_interval(self._hass, lambda now: self._update(),
DEFAULT_UPDATE_INTERVAL)
def _update(self, count=1):
"""Remove events and add new random events."""
# Remove devices.
for _ in range(1, count + 1):
if self._managed_devices:
device = random.choice(self._managed_devices)
if device:
_LOGGER.debug("Removing %s", device)
self._managed_devices.remove(device)
self._hass.add_job(device.async_remove())
# Generate new devices from events.
new_devices = []
for _ in range(1, count + 1):
new_device = self._generate_random_event()
_LOGGER.debug("Adding %s", new_device)
new_devices.append(new_device)
self._managed_devices.append(new_device)
self._add_entities(new_devices)
class DemoGeoLocationEvent(GeoLocationEvent):
"""This represents a demo geo location event."""
def __init__(self, name, distance, latitude, longitude,
unit_of_measurement):
"""Initialize entity with data provided."""
self._name = name
self._distance = distance
self._latitude = latitude
self._longitude = longitude
self._unit_of_measurement = unit_of_measurement
@property
def name(self) -> Optional[str]:
"""Return the name of the event."""
return self._name
@property
def should_poll(self):
"""No polling needed for a demo geo location event."""
return False
@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return self._distance
@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return self._latitude
@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return self._longitude
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement

View File

@@ -0,0 +1,158 @@
"""
The Habitica API component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/habitica/
"""
import logging
from collections import namedtuple
import voluptuous as vol
from homeassistant.const import \
CONF_NAME, CONF_URL, CONF_SENSORS, CONF_PATH, CONF_API_KEY
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import \
config_validation as cv, discovery
REQUIREMENTS = ['habitipy==0.2.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "habitica"
CONF_API_USER = "api_user"
ST = SensorType = namedtuple('SensorType', [
"name", "icon", "unit", "path"
])
SENSORS_TYPES = {
'name': ST('Name', None, '', ["profile", "name"]),
'hp': ST('HP', 'mdi:heart', 'HP', ["stats", "hp"]),
'maxHealth': ST('max HP', 'mdi:heart', 'HP', ["stats", "maxHealth"]),
'mp': ST('Mana', 'mdi:auto-fix', 'MP', ["stats", "mp"]),
'maxMP': ST('max Mana', 'mdi:auto-fix', 'MP', ["stats", "maxMP"]),
'exp': ST('EXP', 'mdi:star', 'EXP', ["stats", "exp"]),
'toNextLevel': ST(
'Next Lvl', 'mdi:star', 'EXP', ["stats", "toNextLevel"]),
'lvl': ST(
'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ["stats", "lvl"]),
'gp': ST('Gold', 'mdi:coin', 'Gold', ["stats", "gp"]),
'class': ST('Class', 'mdi:sword', '', ["stats", "class"])
}
INSTANCE_SCHEMA = vol.Schema({
vol.Optional(CONF_URL, default='https://habitica.com'): cv.url,
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_USER): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)):
vol.All(
cv.ensure_list,
vol.Unique(),
[vol.In(list(SENSORS_TYPES))])
})
has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name
# because we want a handy alias
def has_all_unique_users(value):
"""Validate that all `api_user`s are unique."""
api_users = [user[CONF_API_USER] for user in value]
has_unique_values(api_users)
return value
def has_all_unique_users_names(value):
"""Validate that all user's names are unique and set if any is set."""
names = [user.get(CONF_NAME) for user in value]
if None in names and any(name is not None for name in names):
raise vol.Invalid(
'user names of all users must be set if any is set')
if not all(name is None for name in names):
has_unique_values(names)
return value
INSTANCE_LIST_SCHEMA = vol.All(
cv.ensure_list,
has_all_unique_users,
has_all_unique_users_names,
[INSTANCE_SCHEMA])
CONFIG_SCHEMA = vol.Schema({
DOMAIN: INSTANCE_LIST_SCHEMA
}, extra=vol.ALLOW_EXTRA)
SERVICE_API_CALL = 'api_call'
ATTR_NAME = CONF_NAME
ATTR_PATH = CONF_PATH
ATTR_ARGS = "args"
EVENT_API_CALL_SUCCESS = "{0}_{1}_{2}".format(
DOMAIN, SERVICE_API_CALL, "success")
SERVICE_API_CALL_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): str,
vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_ARGS): dict
})
async def async_setup(hass, config):
"""Set up the habitica service."""
conf = config[DOMAIN]
data = hass.data[DOMAIN] = {}
websession = async_get_clientsession(hass)
from habitipy.aio import HabitipyAsync
class HAHabitipyAsync(HabitipyAsync):
"""Closure API class to hold session."""
def __call__(self, **kwargs):
return super().__call__(websession, **kwargs)
for instance in conf:
url = instance[CONF_URL]
username = instance[CONF_API_USER]
password = instance[CONF_API_KEY]
name = instance.get(CONF_NAME)
config_dict = {"url": url, "login": username, "password": password}
api = HAHabitipyAsync(config_dict)
user = await api.user.get()
if name is None:
name = user['profile']['name']
data[name] = api
if CONF_SENSORS in instance:
hass.async_create_task(
discovery.async_load_platform(
hass, "sensor", DOMAIN,
{"name": name, "sensors": instance[CONF_SENSORS]},
config))
async def handle_api_call(call):
name = call.data[ATTR_NAME]
path = call.data[ATTR_PATH]
api = hass.data[DOMAIN].get(name)
if api is None:
_LOGGER.error(
"API_CALL: User '%s' not configured", name)
return
try:
for element in path:
api = api[element]
except KeyError:
_LOGGER.error(
"API_CALL: Path %s is invalid"
" for api on '{%s}' element", path, element)
return
kwargs = call.data.get(ATTR_ARGS, {})
data = await api(**kwargs)
hass.bus.async_fire(EVENT_API_CALL_SUCCESS, {
"name": name, "path": path, "data": data
})
hass.services.async_register(
DOMAIN, SERVICE_API_CALL,
handle_api_call,
schema=SERVICE_API_CALL_SCHEMA)
return True

View File

@@ -0,0 +1,15 @@
# Describes the format for Habitica service
---
api_call:
description: Call Habitica api
fields:
name:
description: Habitica's username to call for
example: 'xxxNotAValidNickxxx'
path:
description: "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks"
example: '["tasks", "user", "post"]'
args:
description: Any additional json or url parameter arguments. See apidoc mentioned for path. Example uses same api endpoint
example: '{"text": "Use API from Home Assistant", "type": "todo"}'

View File

@@ -5,17 +5,25 @@
"unknown": "Une erreur inconnue s'est produite"
},
"error": {
"invalid_2fa": "Authentification \u00e0 2 facteurs invalide, veuillez r\u00e9essayer.",
"invalid_2fa_method": "M\u00e9thode 2FA non valide (v\u00e9rifiez sur le t\u00e9l\u00e9phone).",
"invalid_login": "Login invalide, veuillez r\u00e9essayer."
},
"step": {
"2fa": {
"data": {
"2fa": "Code PIN d'authentification \u00e0 2 facteurs"
},
"title": "Authentification \u00e0 2 facteurs"
},
"user": {
"data": {
"email": "Adresse e-mail",
"password": "Mot de passe"
}
},
"title": "Connexion \u00e0 Google Hangouts"
}
}
},
"title": "Google Hangouts"
}
}

View File

@@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "A Google Hangouts m\u00e1r konfigur\u00e1lva van",
"unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt."
},
"error": {
"invalid_2fa": "\u00c9rv\u00e9nytelen K\u00e9tfaktoros hiteles\u00edt\u00e9s, pr\u00f3b\u00e1ld \u00fajra.",
"invalid_2fa_method": "\u00c9rv\u00e9nytelen 2FA M\u00f3dszer (Ellen\u0151rz\u00e9s a Telefonon).",
"invalid_login": "\u00c9rv\u00e9nytelen bejelentkez\u00e9s, pr\u00f3b\u00e1ld \u00fajra."
},
"step": {
"2fa": {
"data": {
"2fa": "2FA Pin"
},
"title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s"
},
"user": {
"data": {
"email": "E-Mail C\u00edm",
"password": "Jelsz\u00f3"
},
"title": "Google Hangouts Bejelentkez\u00e9s"
}
},
"title": "Google Hangouts"
}
}

View File

@@ -1,7 +1,11 @@
{
"config": {
"abort": {
"already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado."
"already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado.",
"unknown": "Ocorreu um erro desconhecido."
},
"error": {
"invalid_2fa": "Autentica\u00e7\u00e3o de 2 fatores inv\u00e1lida, por favor, tente novamente."
},
"step": {
"2fa": {

View File

@@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "Google Hangouts \u00e4r redan inst\u00e4llt",
"unknown": "Ett ok\u00e4nt fel intr\u00e4ffade"
},
"error": {
"invalid_2fa": "Ogiltig 2FA autentisering, f\u00f6rs\u00f6k igen.",
"invalid_2fa_method": "Ogiltig 2FA-metod (Verifiera med telefon).",
"invalid_login": "Ogiltig inloggning, f\u00f6rs\u00f6k igen."
},
"step": {
"2fa": {
"data": {
"2fa": "2FA Pinkod"
},
"title": "Tv\u00e5faktorsautentisering"
},
"user": {
"data": {
"email": "E-postadress",
"password": "L\u00f6senord"
},
"title": "Google Hangouts-inloggning"
}
},
"title": "Google Hangouts"
}
}

View File

@@ -6,7 +6,7 @@
},
"error": {
"invalid_login": "Invalid Login, please try again.",
"invalid_2fa": "Invalid 2 Factor Authorization, please try again.",
"invalid_2fa": "Invalid 2 Factor Authentication, please try again.",
"invalid_2fa_method": "Invalid 2FA Method (Verify on Phone)."
},
"step": {
@@ -23,7 +23,7 @@
"2fa": "2FA Pin"
},
"description": "",
"title": "2-Factor-Authorization"
"title": "2-Factor-Authentication"
}
},
"title": "Google Hangouts"

View File

@@ -77,7 +77,7 @@ HM_DEVICE_TYPES = {
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor',
'IPKeySwitchPowermeter'],
'IPKeySwitchPowermeter', 'IPThermostatWall230V'],
DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',

View File

@@ -2,6 +2,13 @@
"config": {
"error": {
"invalid_pin": "Ugyldig PIN, pr\u00f8v igen."
},
"step": {
"init": {
"data": {
"pin": "Pin kode (valgfri)"
}
}
}
}
}

View File

@@ -1,12 +1,16 @@
{
"config": {
"abort": {
"unknown": "Une erreur inconnue s'est produite"
"already_configured": "Le point d'acc\u00e8s est d\u00e9j\u00e0 configur\u00e9",
"conection_aborted": "Impossible de se connecter au serveur HMIP",
"connection_aborted": "Impossible de se connecter au serveur HMIP",
"unknown": "Une erreur inconnue s'est produite."
},
"error": {
"invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.",
"press_the_button": "Veuillez appuyer sur le bouton bleu.",
"register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer."
"register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer.",
"timeout_button": "D\u00e9lai d'attente expir\u00e9, veuillez r\u00e9\u00e9ssayer."
},
"step": {
"init": {
@@ -14,8 +18,14 @@
"hapid": "ID du point d'acc\u00e8s (SGTIN)",
"name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les p\u00e9riph\u00e9riques)",
"pin": "Code PIN (facultatif)"
}
},
"title": "Choisissez le point d'acc\u00e8s HomematicIP"
},
"link": {
"description": "Appuyez sur le bouton bleu du point d'acc\u00e8s et sur le bouton Envoyer pour enregistrer HomematicIP avec Home Assistant. \n\n ![Emplacement du bouton sur le pont](/static/images/config_flows/config_homematicip_cloud.png)",
"title": "Lier le point d'acc\u00e8s"
}
}
},
"title": "HomematicIP Cloud"
}
}

View File

@@ -5,6 +5,7 @@
"connection_aborted": "Impossibile connettersi al server HMIP"
},
"error": {
"invalid_pin": "PIN non valido, riprova.",
"press_the_button": "Si prega di premere il pulsante blu.",
"register_failed": "Registrazione fallita, si prega di riprovare."
},

View File

@@ -22,7 +22,7 @@
"title": "Velg HomematicIP tilgangspunkt"
},
"link": {
"description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)",
"description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og p\u00e5 send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)",
"title": "Link tilgangspunkt"
}
},

View File

@@ -3,6 +3,7 @@
"abort": {
"already_configured": "Accesspunkten \u00e4r redan konfigurerad",
"conection_aborted": "Kunde inte ansluta till HMIP server",
"connection_aborted": "Det gick inte att ansluta till HMIP-servern",
"unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat"
},
"error": {

View File

@@ -24,6 +24,6 @@
"title": "Hub de liaison"
}
},
"title": "Pont Philips Hue"
"title": "Philips Hue"
}
}

View File

@@ -11,7 +11,8 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_FILENAME, CONF_HOST
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers import (
aiohttp_client, config_validation as cv, device_registry as dr)
from .const import DOMAIN, API_NUPNP
from .bridge import HueBridge
@@ -132,7 +133,28 @@ async def async_setup_entry(hass, entry):
bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
hass.data[DOMAIN][host] = bridge
return await bridge.async_setup()
if not await bridge.async_setup():
return False
config = bridge.api.config
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
config_entry=entry.entry_id,
connections={
(dr.CONNECTION_NETWORK_MAC, config.mac)
},
identifiers={
(DOMAIN, config.bridgeid)
},
manufacturer='Signify',
name=config.name,
# Not yet exposed as properties in aiohue
model=config.raw['modelid'],
sw_version=config.raw['swversion'],
)
return True
async def async_unload_entry(hass, entry):

View File

@@ -7,6 +7,8 @@ https://home-assistant.io/components/insteon/
import asyncio
import collections
import logging
from typing import Dict
import voluptuous as vol
from homeassistant.core import callback
@@ -18,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['insteonplm==0.12.3']
REQUIREMENTS = ['insteonplm==0.14.2']
_LOGGER = logging.getLogger(__name__)
@@ -27,9 +29,9 @@ DOMAIN = 'insteon'
CONF_IP_PORT = 'ip_port'
CONF_HUB_USERNAME = 'username'
CONF_HUB_PASSWORD = 'password'
CONF_HUB_VERSION = 'hub_version'
CONF_OVERRIDE = 'device_override'
CONF_PLM_HUB_MSG = ('Must configure either a PLM port or a Hub host, username '
'and password')
CONF_PLM_HUB_MSG = 'Must configure either a PLM port or a Hub host'
CONF_ADDRESS = 'address'
CONF_CAT = 'cat'
CONF_SUBCAT = 'subcat'
@@ -66,6 +68,22 @@ EVENT_BUTTON_ON = 'insteon.button_on'
EVENT_BUTTON_OFF = 'insteon.button_off'
EVENT_CONF_BUTTON = 'button'
def set_default_port(schema: Dict) -> Dict:
"""Set the default port based on the Hub version."""
# If the ip_port is found do nothing
# If it is not found the set the default
ip_port = schema.get(CONF_IP_PORT)
if not ip_port:
hub_version = schema.get(CONF_HUB_VERSION)
# Found hub_version but not ip_port
if hub_version == 1:
schema[CONF_IP_PORT] = 9761
else:
schema[CONF_IP_PORT] = 25105
return schema
CONF_DEVICE_OVERRIDE_SCHEMA = vol.All(
cv.deprecated(CONF_PLATFORM), vol.Schema({
vol.Required(CONF_ADDRESS): cv.string,
@@ -88,12 +106,13 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(
vol.Schema(
{vol.Exclusive(CONF_PORT, 'plm_or_hub',
msg=CONF_PLM_HUB_MSG): cv.isdevice,
msg=CONF_PLM_HUB_MSG): cv.string,
vol.Exclusive(CONF_HOST, 'plm_or_hub',
msg=CONF_PLM_HUB_MSG): cv.string,
vol.Optional(CONF_IP_PORT, default=25105): int,
vol.Optional(CONF_IP_PORT): cv.port,
vol.Optional(CONF_HUB_USERNAME): cv.string,
vol.Optional(CONF_HUB_PASSWORD): cv.string,
vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]),
vol.Optional(CONF_OVERRIDE): vol.All(
cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]),
vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES),
@@ -103,14 +122,7 @@ CONFIG_SCHEMA = vol.Schema({
[CONF_X10_SCHEMA])
}, extra=vol.ALLOW_EXTRA, required=True),
cv.has_at_least_one_key(CONF_PORT, CONF_HOST),
vol.Schema(
{vol.Inclusive(CONF_HOST, 'hub',
msg=CONF_PLM_HUB_MSG): cv.string,
vol.Inclusive(CONF_HUB_USERNAME, 'hub',
msg=CONF_PLM_HUB_MSG): cv.string,
vol.Inclusive(CONF_HUB_PASSWORD, 'hub',
msg=CONF_PLM_HUB_MSG): cv.string,
}, extra=vol.ALLOW_EXTRA, required=True))
set_default_port)
}, extra=vol.ALLOW_EXTRA)
@@ -151,6 +163,7 @@ def async_setup(hass, config):
ip_port = conf.get(CONF_IP_PORT)
username = conf.get(CONF_HUB_USERNAME)
password = conf.get(CONF_HUB_PASSWORD)
hub_version = conf.get(CONF_HUB_VERSION)
overrides = conf.get(CONF_OVERRIDE, [])
x10_devices = conf.get(CONF_X10, [])
x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF)
@@ -284,6 +297,7 @@ def async_setup(hass, config):
port=ip_port,
username=username,
password=password,
hub_version=hub_version,
loop=hass.loop,
workdir=hass.config.config_dir)
else:
@@ -358,6 +372,8 @@ class IPDB:
def __init__(self):
"""Create the INSTEON Product Database (IPDB)."""
from insteonplm.states.cover import Cover
from insteonplm.states.onOff import (OnOffSwitch,
OnOffSwitch_OutletTop,
OnOffSwitch_OutletBottom,
@@ -383,7 +399,9 @@ class IPDB:
X10AllLightsOnSensor,
X10AllLightsOffSensor)
self.states = [State(OnOffSwitch_OutletTop, 'switch'),
self.states = [State(Cover, 'cover'),
State(OnOffSwitch_OutletTop, 'switch'),
State(OnOffSwitch_OutletBottom, 'switch'),
State(OpenClosedRelay, 'switch'),
State(OnOffSwitch, 'switch'),
@@ -470,11 +488,10 @@ class InsteonEntity(Entity):
return attributes
@callback
def async_entity_update(self, deviceid, statename, val):
def async_entity_update(self, deviceid, group, val):
"""Receive notification from transport that new data exists."""
_LOGGER.debug('Received update for device %s group %d statename %s',
self.address, self.group,
self._insteon_device_state.name)
_LOGGER.debug('Received update for device %s group %d value %s',
deviceid.human, group, val)
self.async_schedule_update_ha_state()
@asyncio.coroutine

View File

@@ -0,0 +1,14 @@
{
"config": {
"abort": {
"single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Home Assistant iOS."
},
"step": {
"confirm": {
"description": "Voleu configurar el component Home Assistant iOS?",
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@@ -0,0 +1,10 @@
{
"config": {
"step": {
"confirm": {
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@@ -0,0 +1,14 @@
{
"config": {
"abort": {
"single_instance_allowed": "Seule une configuration de Home Assistant iOS est n\u00e9cessaire."
},
"step": {
"confirm": {
"description": "Voulez-vous configurer le composant Home Assistant iOS?",
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@@ -0,0 +1,14 @@
{
"config": {
"abort": {
"single_instance_allowed": "\ud558\ub098\uc758 Home Assistant iOS \uad6c\uc131\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
},
"step": {
"confirm": {
"description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc124\uc815\uc744 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@@ -0,0 +1,14 @@
{
"config": {
"abort": {
"single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Home Assistant iOS nodig."
},
"step": {
"confirm": {
"description": "Wilt u het Home Assistant iOS component instellen?",
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@@ -0,0 +1,14 @@
{
"config": {
"abort": {
"single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Home Assistant iOS."
},
"step": {
"confirm": {
"description": "Czy chcesz skonfigurowa\u0107 Home Assistant iOS?",
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@@ -0,0 +1,14 @@
{
"config": {
"abort": {
"single_instance_allowed": "Home Assistant iOS \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002"
},
"step": {
"confirm": {
"description": "\u662f\u5426\u8981\u8bbe\u7f6e Home Assistant iOS \u7ec4\u4ef6\uff1f",
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@@ -82,6 +82,11 @@ class DeconzLight(Light):
self._light.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._light.deconz_id
async def async_will_remove_from_hass(self) -> None:
"""Disconnect light object when removed."""
self._light.remove_callback(self.async_update_callback)
self._light = None
@callback
def async_update_callback(self, reason):
"""Update the light's state."""

View File

@@ -285,6 +285,25 @@ class HueLight(Light):
"""Return the list of supported effects."""
return [EFFECT_COLORLOOP, EFFECT_RANDOM]
@property
def device_info(self):
"""Return the device info."""
if self.light.type in ('LightGroup', 'Room'):
return None
return {
'identifiers': {
(hue.DOMAIN, self.unique_id)
},
'name': self.name,
'manufacturer': self.light.manufacturername,
# productname added in Hue Bridge API 1.24
# (published 03/05/2018)
'model': self.light.productname or self.light.modelid,
# Not yet exposed as properties in aiohue
'sw_version': self.light.raw['swversion'],
}
async def async_turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
command = {'on': True}

View File

@@ -54,6 +54,7 @@ CONF_WHITE_VALUE_SCALE = 'white_value_scale'
CONF_WHITE_VALUE_STATE_TOPIC = 'white_value_state_topic'
CONF_WHITE_VALUE_TEMPLATE = 'white_value_template'
CONF_ON_COMMAND_TYPE = 'on_command_type'
CONF_UNIQUE_ID = 'unique_id'
DEFAULT_BRIGHTNESS_SCALE = 255
DEFAULT_NAME = 'MQTT Light'
@@ -79,6 +80,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
@@ -111,6 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities,
async_add_entities([MqttLight(
config.get(CONF_NAME),
config.get(CONF_UNIQUE_ID),
config.get(CONF_EFFECT_LIST),
{
key: config.get(key) for key in (
@@ -159,14 +162,15 @@ async def async_setup_platform(hass, config, async_add_entities,
class MqttLight(MqttAvailability, Light):
"""Representation of a MQTT light."""
def __init__(self, name, effect_list, topic, templates, qos,
retain, payload, optimistic, brightness_scale,
def __init__(self, name, unique_id, effect_list, topic, templates,
qos, retain, payload, optimistic, brightness_scale,
white_value_scale, on_command_type, availability_topic,
payload_available, payload_not_available):
"""Initialize MQTT light."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
self._name = name
self._unique_id = unique_id
self._effect_list = effect_list
self._topic = topic
self._qos = qos
@@ -392,6 +396,11 @@ class MqttLight(MqttAvailability, Light):
"""Return the name of the device if any."""
return self._name
@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@property
def is_on(self):
"""Return true if device is on."""

View File

@@ -19,7 +19,7 @@ from homeassistant.util.color import \
from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired)
REQUIREMENTS = ['pyHS100==0.3.2']
REQUIREMENTS = ['pyHS100==0.3.3']
_LOGGER = logging.getLogger(__name__)

View File

@@ -4,31 +4,33 @@ Provides functionality for mailboxes.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mailbox/
"""
import asyncio
import logging
from contextlib import suppress
from datetime import timedelta
import async_timeout
import logging
from aiohttp import web
from aiohttp.web_exceptions import HTTPNotFound
import async_timeout
from homeassistant.core import callback
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.setup import async_prepare_setup_platform
_LOGGER = logging.getLogger(__name__)
CONTENT_TYPE_MPEG = 'audio/mpeg'
DEPENDENCIES = ['http']
DOMAIN = 'mailbox'
EVENT = 'mailbox_updated'
CONTENT_TYPE_MPEG = 'audio/mpeg'
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine

View File

@@ -7,17 +7,18 @@ https://home-assistant.io/components/mailbox.asteriskvm/
import asyncio
import logging
from homeassistant.core import callback
from homeassistant.components.asterisk_mbox import DOMAIN
from homeassistant.components.mailbox import (Mailbox, CONTENT_TYPE_MPEG,
StreamError)
from homeassistant.components.mailbox import (
CONTENT_TYPE_MPEG, Mailbox, StreamError)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['asterisk_mbox']
_LOGGER = logging.getLogger(__name__)
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
DEPENDENCIES = ['asterisk_mbox']
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
@asyncio.coroutine

View File

@@ -5,16 +5,16 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/mailbox.asteriskvm/
"""
import asyncio
from hashlib import sha1
import logging
import os
from hashlib import sha1
from homeassistant.components.mailbox import (
CONTENT_TYPE_MPEG, Mailbox, StreamError)
from homeassistant.util import dt
from homeassistant.components.mailbox import (Mailbox, CONTENT_TYPE_MPEG,
StreamError)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "DemoMailbox"
@@ -38,11 +38,15 @@ class DemoMailbox(Mailbox):
msgtxt = "Message {}. {}".format(
idx + 1, txt * (1 + idx * (idx % 2)))
msgsha = sha1(msgtxt.encode('utf-8')).hexdigest()
msg = {"info": {"origtime": msgtime,
"callerid": "John Doe <212-555-1212>",
"duration": "10"},
"text": msgtxt,
"sha": msgsha}
msg = {
'info': {
'origtime': msgtime,
'callerid': 'John Doe <212-555-1212>',
'duration': '10',
},
'text': msgtxt,
'sha': msgsha,
}
self._messages[msgsha] = msg
@property

View File

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

View File

@@ -15,33 +15,32 @@ from random import SystemRandom
from urllib.parse import urlparse
from aiohttp import web
from aiohttp.hdrs import CONTENT_TYPE, CACHE_CONTROL
from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE
import async_timeout
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
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)
ATTR_ENTITY_ID, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, SERVICE_SHUFFLE_SET,
SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE,
STATE_OFF, STATE_PLAYING, STATE_UNKNOWN)
from homeassistant.core import callback
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.components import websocket_api
_LOGGER = logging.getLogger(__name__)
_RND = SystemRandom()
DOMAIN = 'media_player'
DEPENDENCIES = ['http']
SCAN_INTERVAL = timedelta(seconds=10)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@@ -97,6 +96,8 @@ MEDIA_TYPE_CHANNEL = 'channel'
MEDIA_TYPE_PLAYLIST = 'playlist'
MEDIA_TYPE_URL = 'url'
SCAN_INTERVAL = timedelta(seconds=10)
SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2
SUPPORT_VOLUME_SET = 4

View File

@@ -4,17 +4,17 @@ Support for Anthem Network Receivers and Processors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.anthemav/
"""
import logging
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE,
PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, STATE_UNKNOWN,
EVENT_HOMEASSISTANT_STOP)
CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF,
STATE_ON, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['anthemav==1.1.8']
@@ -32,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
})
@asyncio.coroutine

View File

@@ -7,20 +7,19 @@ https://home-assistant.io/components/media_player.apple_tv/
import asyncio
import logging
from homeassistant.core import callback
from homeassistant.components.apple_tv import (
ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES)
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_STOP, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_ON,
SUPPORT_TURN_OFF, MediaPlayerDevice, MEDIA_TYPE_MUSIC,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW)
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST,
STATE_OFF, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF,
STATE_PAUSED, STATE_PLAYING, STATE_STANDBY)
from homeassistant.core import callback
import homeassistant.util.dt as dt_util
DEPENDENCIES = ['apple_tv']
_LOGGER = logging.getLogger(__name__)
@@ -31,8 +30,8 @@ SUPPORT_APPLE_TV = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the Apple TV platform."""
if not discovery_info:
return

View File

@@ -9,16 +9,13 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_SELECT_SOURCE,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY,
SUPPORT_VOLUME_SET, MediaPlayerDevice, PLATFORM_SCHEMA)
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN,
CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT)
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT,
CONF_USERNAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['sharp_aquos_rc==0.3.2']

View File

@@ -13,15 +13,15 @@ from homeassistant.components.media_player import (
DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON)
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_OFF,
STATE_ON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyblackbird==0.5']
_LOGGER = logging.getLogger(__name__)
SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_SELECT_SOURCE
SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
@@ -33,7 +33,6 @@ SOURCE_SCHEMA = vol.Schema({
CONF_ZONES = 'zones'
CONF_SOURCES = 'sources'
CONF_TYPE = 'type'
DATA_BLACKBIRD = 'blackbird'

View File

@@ -24,8 +24,8 @@ from homeassistant.components.media_player import (
MediaPlayerDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE,
STATE_OFF, STATE_PAUSED, STATE_PLAYING)
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF,
STATE_PAUSED, STATE_PLAYING)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv

View File

@@ -10,11 +10,11 @@ import re
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY,
SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice,
PLATFORM_SCHEMA)
from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json

View File

@@ -7,26 +7,27 @@ https://home-assistant.io/components/media_player.cast/
import asyncio
import logging
import threading
from typing import Optional, Tuple
import voluptuous as vol
import attr
import voluptuous as vol
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (dispatcher_send,
async_dispatcher_connect)
from homeassistant.components.cast import DOMAIN as CAST_DOMAIN
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
EVENT_HOMEASSISTANT_STOP)
CONF_HOST, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED,
STATE_PLAYING)
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, dispatcher_send)
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util
DEPENDENCIES = ('cast',)
@@ -57,10 +58,14 @@ SIGNAL_CAST_DISCOVERED = 'cast_discovered'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list,
[cv.string])
vol.Optional(CONF_IGNORE_CEC, default=[]):
vol.All(cv.ensure_list, [cv.string]),
})
CONNECTION_RETRY = 3
CONNECTION_RETRY_WAIT = 2
CONNECTION_TIMEOUT = 10
@attr.s(slots=True, frozen=True)
class ChromecastInfo:
@@ -73,7 +78,8 @@ class ChromecastInfo:
port = attr.ib(type=int)
uuid = attr.ib(type=Optional[str], converter=attr.converters.optional(str),
default=None) # always convert UUID to string if not None
model_name = attr.ib(type=str, default='') # needed for cast type
manufacturer = attr.ib(type=str, default='')
model_name = attr.ib(type=str, default='')
friendly_name = attr.ib(type=Optional[str], default=None)
@property
@@ -111,6 +117,7 @@ def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo:
host=info.host, port=info.port,
uuid=(info.uuid or http_device_status.uuid),
friendly_name=(info.friendly_name or http_device_status.friendly_name),
manufacturer=(info.manufacturer or http_device_status.manufacturer),
model_name=(info.model_name or http_device_status.model_name)
)
@@ -148,7 +155,13 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None:
def internal_callback(name):
"""Handle zeroconf discovery of a new chromecast."""
mdns = listener.services[name]
_discover_chromecast(hass, ChromecastInfo(*mdns))
_discover_chromecast(hass, ChromecastInfo(
host=mdns[0],
port=mdns[1],
uuid=mdns[2],
model_name=mdns[3],
friendly_name=mdns[4],
))
_LOGGER.debug("Starting internal pychromecast discovery.")
listener, browser = pychromecast.start_discovery(internal_callback)
@@ -356,16 +369,18 @@ class CastDevice(MediaPlayerDevice):
if self._chromecast is not None:
if old_cast_info.host_port == cast_info.host_port:
# Nothing connection-related updated
_LOGGER.debug("No connection related update: %s",
cast_info.host_port)
return
await self._async_disconnect()
# Failed connection will unfortunately never raise an exception, it
# will instead just try connecting indefinitely.
# pylint: disable=protected-access
_LOGGER.debug("Connecting to cast device %s", cast_info)
chromecast = await self.hass.async_add_job(
pychromecast._get_chromecast_from_host, attr.astuple(cast_info))
pychromecast._get_chromecast_from_host, (
cast_info.host, cast_info.port, cast_info.uuid,
cast_info.model_name, cast_info.friendly_name
), CONNECTION_RETRY, CONNECTION_RETRY_WAIT, CONNECTION_TIMEOUT)
self._chromecast = chromecast
self._status_listener = CastStatusListener(self, chromecast)
# Initialise connection status as connected because we can only
@@ -389,7 +404,12 @@ class CastDevice(MediaPlayerDevice):
await self.hass.async_add_job(self._chromecast.disconnect)
# Invalidate some attributes
self._invalidate()
self.async_schedule_update_ha_state()
def _invalidate(self):
"""Invalidate some attributes."""
self._chromecast = None
self.cast_status = None
self.media_status = None
@@ -398,8 +418,6 @@ class CastDevice(MediaPlayerDevice):
self._status_listener.invalidate()
self._status_listener = None
self.async_schedule_update_ha_state()
# ========== Callbacks ==========
def new_cast_status(self, cast_status):
"""Handle updates of the cast status."""
@@ -414,7 +432,16 @@ class CastDevice(MediaPlayerDevice):
def new_connection_status(self, connection_status):
"""Handle updates of connection status."""
from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED
from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, \
CONNECTION_STATUS_DISCONNECTED
_LOGGER.debug("Received cast device connection status: %s",
connection_status.status)
if connection_status.status == CONNECTION_STATUS_DISCONNECTED:
self._available = False
self._invalidate()
self.schedule_update_ha_state()
return
new_available = connection_status.status == CONNECTION_STATUS_CONNECTED
if new_available != self._available:
@@ -494,6 +521,23 @@ class CastDevice(MediaPlayerDevice):
"""Return the name of the device."""
return self._cast_info.friendly_name
@property
def device_info(self):
"""Return information about the device."""
cast_info = self._cast_info
if cast_info.model_name == "Google Cast Group":
return None
return {
'name': cast_info.friendly_name,
'identifiers': {
(CAST_DOMAIN, cast_info.uuid.replace('-', ''))
},
'model': cast_info.model_name,
'manufacturer': cast_info.manufacturer,
}
@property
def state(self):
"""Return the state of the player."""

View File

@@ -9,18 +9,18 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE,
MEDIA_TYPE_MOVIE, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA,
DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE,
MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_VOLUME_MUTE,
MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING,
ATTR_ENTITY_ID)
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_PAUSED,
STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pychannels==1.0.0']
_LOGGER = logging.getLogger(__name__)
DATA_CHANNELS = 'channels'
@@ -52,16 +52,11 @@ CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({
vol.Required(ATTR_SECONDS): vol.Coerce(int),
})
REQUIREMENTS = ['pychannels==1.0.0']
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Channels platform."""
device = ChannelsPlayer(
config.get('name'),
config.get(CONF_HOST),
config.get(CONF_PORT)
)
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT))
if DATA_CHANNELS not in hass.data:
hass.data[DATA_CHANNELS] = []
@@ -77,8 +72,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
device.entity_id == entity_id), None)
if device is None:
_LOGGER.warning("Unable to find Channels with entity_id: %s",
entity_id)
_LOGGER.warning(
"Unable to find Channels with entity_id: %s", entity_id)
return
if service.service == SERVICE_SEEK_FORWARD:
@@ -90,12 +85,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
device.seek_by(seconds)
hass.services.register(
DOMAIN, SERVICE_SEEK_FORWARD, service_handler,
schema=CHANNELS_SCHEMA)
DOMAIN, SERVICE_SEEK_FORWARD, service_handler, schema=CHANNELS_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_SEEK_BACKWARD, service_handler,
schema=CHANNELS_SCHEMA)
DOMAIN, SERVICE_SEEK_BACKWARD, service_handler, schema=CHANNELS_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_SEEK_BY, service_handler,

View File

@@ -4,7 +4,6 @@ Support for Clementine Music Player as media player.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.clementine/
"""
import asyncio
from datetime import timedelta
import logging
@@ -12,24 +11,24 @@ import time
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA,
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MEDIA_TYPE_MUSIC,
SUPPORT_VOLUME_SET, MediaPlayerDevice)
MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, CONF_ACCESS_TOKEN,
STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF,
STATE_PAUSED, STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-clementine-remote==1.0.1']
SCAN_INTERVAL = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Clementine Remote'
DEFAULT_PORT = 5500
SCAN_INTERVAL = timedelta(seconds=5)
SUPPORT_CLEMENTINE = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_VOLUME_SET | \
SUPPORT_NEXT_TRACK | \
@@ -69,7 +68,7 @@ class ClementineDevice(MediaPlayerDevice):
self._track_name = ''
self._track_artist = ''
self._track_album_name = ''
self._state = STATE_UNKNOWN
self._state = None
def update(self):
"""Retrieve the latest data from the Clementine Player."""

View File

@@ -8,15 +8,14 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PLAY,
SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK, PLATFORM_SCHEMA,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT,
CONF_PASSWORD)
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED,
STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pycmus==0.1.1']

View File

@@ -5,11 +5,12 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_PLAY, SUPPORT_SHUFFLE_SET, MediaPlayerDevice)
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
import homeassistant.util.dt as dt_util
@@ -20,8 +21,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
DemoYoutubePlayer(
'Living Room', 'eyU3bRy2x44',
'♥♥ The Best Fireplace Video (3 hours)', 300),
DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours',
360000),
DemoYoutubePlayer(
'Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours', 360000),
DemoMusicPlayer(), DemoTVShowPlayer(),
])

View File

@@ -10,10 +10,10 @@ import telnetlib
import voluptuous as vol
from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE,
SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice)
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv

View File

@@ -5,35 +5,37 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.denon/
"""
import logging
from collections import namedtuple
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE,
SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice,
PLATFORM_SCHEMA, SUPPORT_TURN_ON, MEDIA_TYPE_MUSIC,
SUPPORT_VOLUME_SET, SUPPORT_PLAY)
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
CONF_HOST, CONF_NAME, CONF_TIMEOUT, CONF_ZONE, STATE_OFF, STATE_ON,
STATE_PAUSED, STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.7.5']
_LOGGER = logging.getLogger(__name__)
ATTR_SOUND_MODE_RAW = 'sound_mode_raw'
CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)'
CONF_SHOW_ALL_SOURCES = 'show_all_sources'
CONF_VALID_ZONES = ['Zone2', 'Zone3']
CONF_ZONES = 'zones'
DEFAULT_SHOW_SOURCES = False
DEFAULT_TIMEOUT = 2
CONF_SHOW_ALL_SOURCES = 'show_all_sources'
CONF_ZONES = 'zones'
CONF_VALID_ZONES = ['Zone2', 'Zone3']
CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)'
KEY_DENON_CACHE = 'denonavr_hosts'
ATTR_SOUND_MODE_RAW = 'sound_mode_raw'
KEY_DENON_CACHE = 'denonavr_hosts'
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \

View File

@@ -4,29 +4,28 @@ Support for the DirecTV receivers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.directv/
"""
import voluptuous as vol
import requests
import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
MediaPlayerDevice)
MEDIA_TYPE_MOVIE, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice)
from homeassistant.const import (
CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT)
CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['directpy==0.5']
DEFAULT_DEVICE = '0'
DEFAULT_NAME = 'DirecTV Receiver'
DEFAULT_NAME = "DirecTV Receiver"
DEFAULT_PORT = 8080
SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY
DATA_DIRECTV = "data_directv"
DATA_DIRECTV = 'data_directv'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
@@ -51,9 +50,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
elif discovery_info:
host = discovery_info.get('host')
name = 'DirecTV_' + discovery_info.get('serial', '')
name = 'DirecTV_{}'.format(discovery_info.get('serial', ''))
# attempt to discover additional RVU units
# Attempt to discover additional RVU units
try:
resp = requests.get(
'http://%s:%d/info/getLocations' % (host, DEFAULT_PORT)).json()
@@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
DEFAULT_PORT, loc["clientAddr"]])
except requests.exceptions.RequestException:
# bail out and just go forward with uPnP data
# Bail out and just go forward with uPnP data
if DEFAULT_DEVICE not in known_devices:
hosts.append([name, host, DEFAULT_PORT, DEFAULT_DEVICE])
@@ -78,8 +77,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(dtvs)
hass.data[DATA_DIRECTV] = known_devices
return True
class DirecTvDevice(MediaPlayerDevice):
"""Representation of a DirecTV receiver on the network."""

View File

@@ -1,43 +1,36 @@
# -*- coding: utf-8 -*-
"""
Support for DLNA DMR (Device Media Renderer).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.dlna_dmr/
"""
import asyncio
from datetime import datetime
import functools
import logging
from datetime import datetime
import aiohttp
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MediaPlayerDevice,
PLATFORM_SCHEMA)
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
CONF_URL, CONF_NAME,
STATE_OFF, STATE_ON, STATE_IDLE, STATE_PLAYING, STATE_PAUSED)
CONF_NAME, CONF_URL, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF,
STATE_ON, STATE_PAUSED, STATE_PLAYING)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util import get_local_ip
REQUIREMENTS = ['async-upnp-client==0.12.4']
_LOGGER = logging.getLogger(__name__)
DLNA_DMR_DATA = 'dlna_dmr'
REQUIREMENTS = [
'async-upnp-client==0.12.4',
]
DEFAULT_NAME = 'DLNA Digital Media Renderer'
DEFAULT_LISTEN_PORT = 8301
@@ -68,8 +61,6 @@ HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = {
'playlist': 'playlist/*',
}
_LOGGER = logging.getLogger(__name__)
def catch_request_errors():
"""Catch asyncio.TimeoutError, aiohttp.ClientError errors."""
@@ -96,13 +87,11 @@ async def async_start_event_handler(hass, server_host, server_port, requester):
# start event handler
from async_upnp_client.aiohttp import AiohttpNotifyServer
server = AiohttpNotifyServer(requester,
server_port,
server_host,
hass.loop)
server = AiohttpNotifyServer(
requester, server_port, server_host, hass.loop)
await server.start_server()
_LOGGER.info('UPNP/DLNA event handler listening on: %s',
server.callback_url)
_LOGGER.info(
'UPNP/DLNA event handler listening on: %s', server.callback_url)
hass_data['notify_server'] = server
hass_data['event_handler'] = server.event_handler
@@ -116,10 +105,8 @@ async def async_start_event_handler(hass, server_host, server_port, requester):
return hass_data['event_handler']
async def async_setup_platform(hass: HomeAssistant,
config,
async_add_entities,
discovery_info=None):
async def async_setup_platform(
hass: HomeAssistant, config, async_add_entities, discovery_info=None):
"""Set up DLNA DMR platform."""
if config.get(CONF_URL) is not None:
url = config[CONF_URL]
@@ -145,10 +132,8 @@ async def async_setup_platform(hass: HomeAssistant,
if server_host is None:
server_host = get_local_ip()
server_port = config.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT)
event_handler = await async_start_event_handler(hass,
server_host,
server_port,
requester)
event_handler = await async_start_event_handler(
hass, server_host, server_port, requester)
# create upnp device
from async_upnp_client import UpnpFactory
@@ -183,10 +168,10 @@ class DlnaDmrDevice(MediaPlayerDevice):
"""Handle addition."""
self._device.on_event = self._on_event
# register unsubscribe on stop
# Register unsubscribe on stop
bus = self.hass.bus
bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
self._async_on_hass_stop)
bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self._async_on_hass_stop)
@property
def available(self):
@@ -306,23 +291,21 @@ class DlnaDmrDevice(MediaPlayerDevice):
mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING[media_type]
upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING[media_type]
# stop current playing media
# Stop current playing media
if self._device.can_stop:
await self.async_media_stop()
# queue media
await self._device.async_set_transport_uri(media_id,
title,
mime_type,
upnp_class)
# +ueue media
await self._device.async_set_transport_uri(
media_id, title, mime_type, upnp_class)
await self._device.async_wait_for_can_play()
# if already playing, no need to call Play
# If already playing, no need to call Play
from async_upnp_client import dlna
if self._device.state == dlna.STATE_PLAYING:
return
# play it
# Play it
await self.async_media_play()
@catch_request_errors()

View File

@@ -6,13 +6,13 @@ https://home-assistant.io/components/media_player.dunehd/
"""
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA,
SUPPORT_PLAY, MediaPlayerDevice)
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, STATE_ON, STATE_PLAYING)
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pdunehd==1.3']

View File

@@ -10,13 +10,13 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SEEK, SUPPORT_STOP, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
CONF_HOST, CONF_PORT, CONF_SSL, CONF_API_KEY, DEVICE_DEFAULT_NAME,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF,
STATE_PAUSED, STATE_PLAYING)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
@@ -42,11 +42,11 @@ SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
})
@@ -95,7 +95,7 @@ def async_setup_platform(hass, config, async_add_entities,
if new_devices:
_LOGGER.debug("Adding new devices: %s", new_devices)
async_add_entities(new_devices, update_before_add=True)
async_add_entities(new_devices, True)
@callback
def device_removal_callback(data):

View File

@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/media_player.epson/
"""
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
@@ -20,37 +21,45 @@ import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['epson-projector==0.1.3']
_LOGGER = logging.getLogger(__name__)
ATTR_CMODE = 'cmode'
DATA_EPSON = 'epson'
DEFAULT_NAME = 'EPSON Projector'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=80): cv.port,
vol.Optional(CONF_SSL, default=False): cv.boolean
})
SERVICE_SELECT_CMODE = 'epson_select_cmode'
ATTR_CMODE = 'cmode'
SUPPORT_CMODE = 33001
SUPPORT_EPSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE |\
SUPPORT_CMODE | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \
SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=80): cv.port,
vol.Optional(CONF_SSL, default=False): cv.boolean,
})
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the Epson media player platform."""
from epson_projector.const import (CMODE_LIST_SET)
if DATA_EPSON not in hass.data:
hass.data[DATA_EPSON] = []
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
ssl = config.get(CONF_SSL)
epson = EpsonProjector(async_get_clientsession(
hass, verify_ssl=False), name, host, port, ssl)
epson = EpsonProjector(async_get_clientsession(hass, verify_ssl=False),
name, host,
config.get(CONF_PORT), config.get(CONF_SSL))
hass.data[DATA_EPSON].append(epson)
async_add_entities([epson], update_before_add=True)
@@ -67,7 +76,7 @@ async def async_setup_platform(hass, config, async_add_entities,
cmode = service.data.get(ATTR_CMODE)
await device.select_cmode(cmode)
await device.update()
from epson_projector.const import (CMODE_LIST_SET)
epson_schema = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET))
})
@@ -81,13 +90,12 @@ class EpsonProjector(MediaPlayerDevice):
def __init__(self, websession, name, host, port, encryption):
"""Initialize entity to control Epson projector."""
self._name = name
import epson_projector as epson
from epson_projector.const import DEFAULT_SOURCES
self._name = name
self._projector = epson.Projector(
host,
websession=websession,
port=port)
host, websession=websession, port=port)
self._cmode = None
self._source_list = list(DEFAULT_SOURCES.values())
self._source = None
@@ -97,9 +105,8 @@ class EpsonProjector(MediaPlayerDevice):
async def update(self):
"""Update state of device."""
from epson_projector.const import (
EPSON_CODES, POWER,
CMODE, CMODE_LIST, SOURCE, VOLUME,
BUSY, SOURCE_LIST)
EPSON_CODES, POWER, CMODE, CMODE_LIST, SOURCE, VOLUME, BUSY,
SOURCE_LIST)
is_turned_on = await self._projector.get_property(POWER)
_LOGGER.debug("Project turn on/off status: %s", is_turned_on)
if is_turned_on and is_turned_on == EPSON_CODES[POWER]:

View File

@@ -10,13 +10,13 @@ import requests
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA,
SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice)
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import (
CONF_DEVICE, CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL,
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY,
STATE_UNKNOWN, CONF_HOST, CONF_PORT, CONF_SSL, CONF_NAME, CONF_DEVICE,
CONF_DEVICES)
STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)

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