Compare commits

...

184 Commits

Author SHA1 Message Date
Paulus Schoutsen d1460de89b Merge pull request #14199 from home-assistant/rc
0.68.1
2018-04-30 14:04:29 -04:00
Paulus Schoutsen c23cc0e827 Disable eliqonline requirement (#14156)
* Disable eliqonline requirement

* Disable pylint import error
2018-04-30 13:46:47 -04:00
Paulus Schoutsen c704ceaeb7 Version bump to 0.68.1 2018-04-30 13:37:12 -04:00
Paulus Schoutsen daeccfe764 Fix poorly formatted automations (#14196) 2018-04-30 13:36:56 -04:00
Otto Winter 7f1b591fbb Improve chromecast disconnection logic (#14190)
* Attempt Cast Fix

* Cleanup
2018-04-30 13:36:55 -04:00
Paulus Schoutsen aba143ac9f Do not sync entities with an empty name (#14181) 2018-04-30 13:36:55 -04:00
cdce8p 03c34804bc Added CONF_IP_ADDRESS to HomeKit (#14163) 2018-04-30 13:36:54 -04:00
Matthew Garrett f2a17a5462 Fix Python 3.6 compatibility for HomeKit controller (#14160)
Python 3.6's http client passes an additional argument to _send_output,
so add that to the function definition.
2018-04-30 13:36:54 -04:00
Anders Melchiorsen b5bae17c66 Revert Hue color state to be xy-based (#14154) 2018-04-30 13:36:54 -04:00
Anders Melchiorsen 52a48b3ac9 Improve precision of Hue color state (#14113) 2018-04-30 13:36:53 -04:00
Anders Melchiorsen a06f61034c Fix color setting of tplink lights (#14108) 2018-04-30 13:36:53 -04:00
Paulus Schoutsen 9d34e8c266 Merge pull request #14124 from home-assistant/rc
0.68
2018-04-27 21:44:39 -04:00
Matthew Garrett 7da1d75707 Change Eufy brightness handling (#14111)
Eufy device state isn't reported if the bulb is off, so avoid stamping on
the previous values if the bulb isn't going to give us useful information.
In addition, improve handling of bulb turn on if we aren't provided with a
brightness - this should avoid the bulb tending to end up with a brightness of
1 after power cycling.
2018-04-27 16:41:06 -04:00
Paulus Schoutsen 9fb2bf72f9 Version bump to 0.68.0 2018-04-27 15:35:20 -04:00
GotoCode c42c668815 Updated list of AWS regions for Amazon Polly (#14097)
Fixes #14052
2018-04-27 15:34:57 -04:00
Daniel Høyer Iversen 9d0251cfeb Fix timezone issue when calculating min/max values in tibber #14009 (#14080)
* fix timezone issue in tibber #14009

* remove debug print
2018-04-27 15:34:57 -04:00
Daniel Høyer Iversen 403a546bdc Upgrade broadlink lib (#14074) 2018-04-27 15:34:57 -04:00
Paulus Schoutsen 833508fbbb Version bump to 0.68.0b2 2018-04-26 16:39:42 -04:00
Paulus Schoutsen 1b71ce32e4 Bump frontend to 20180426 2018-04-26 16:39:27 -04:00
Paulus Schoutsen c49751542f Version bump to 0.68.0b1 2018-04-24 23:19:33 -04:00
Mark Coombes 2e3a27e418 Update device classes for contact sensor HomeKit (#14051) 2018-04-24 23:19:16 -04:00
Matthew Garrett 7566bb5aed Handle HomeKit configuration failure more cleanly (#14041)
* Handle HomeKit configuration failure more cleanly

Add support for handling cases where HomeKit configuration fails, and give
the user more information about what to do.

* Don't consume the exception for a homekit.UnknownError

If we get an UnknownError then we should alert the user but also still
generate the backtrace so there's actually something for them to file in
a bug report.
2018-04-24 23:19:16 -04:00
Otto Winter fc1f6ee0f0 Revert cast platform polling mode (#14027) 2018-04-24 23:19:16 -04:00
Matt Schmitt cb839eff0f HomeKit Alarm Control Panel Code Exception Fix (#14025)
* Catch exception for KeyError
* Use get and added test
2018-04-24 23:19:15 -04:00
Paulus Schoutsen 2bc87bfcf0 Order the output of the automation editor (#14019)
* Order the output of the automation editor

* Lint
2018-04-24 23:19:15 -04:00
Johann Kellerman 44be80145b Qwikswitch binary sensors (#14008) 2018-04-24 23:19:15 -04:00
Paulus Schoutsen 8cb1e17ad8 Bump frontend to 20180425.0 2018-04-24 23:18:46 -04:00
Paulus Schoutsen 2b53729708 Version bump to 0.68.0b0 2018-04-20 10:58:43 -04:00
Paulus Schoutsen a566804f7f Merge branch 'dev' into rc 2018-04-20 10:58:25 -04:00
Paulus Schoutsen 2a5fac3b9d Add sensor device classes (#14010) 2018-04-20 15:38:27 +02:00
Rohan Kapoor 8459b241a2 Upgrade pylutron-caseta to 0.5.0 to reestablish connections (#14013)
* Upgrade pylutron-caseta to 0.5.0 to reestablish connections

* Upgrade pylutron-caseta to 0.5.0 in requirements_all.txt
2018-04-20 15:35:56 +02:00
Daniel Høyer Iversen 825f94f47f Tibber available (#13865)
* Tibber available

* Tibber available

* Tibber

* Tibber
2018-04-20 11:45:11 +02:00
Sebastian Muszynski 8ef2abfca7 Log an error instead of raising an exception (#14006) 2018-04-20 08:45:28 +02:00
ChristianKuehnel 2372419d42 Upgraded miflora library to version 0.4.0 (#14005) 2018-04-20 08:43:44 +02:00
Paulus Schoutsen 27f3081b74 Update frontend to 20180420.0 2018-04-19 22:16:48 -04:00
Paulus Schoutsen 13e72f48a8 Disable ebox requirement (#14003)
* Disable ebox requirement

* Lint
2018-04-19 14:06:49 -04:00
Pascal Hahn 9fcbe68fac Add Homematic HmIP-SWO-PR weather sensor support (#13904) 2018-04-19 12:48:21 +02:00
Sebastian Muszynski 0999129f48 Useless code removed (#13996) 2018-04-19 11:42:40 +02:00
Viorel Stirbu 3180c8b0fb Add support for Sensirion SHT31 temperature/humidity sensor (#12952) 2018-04-19 11:37:30 +02:00
koolsb 37cd63ea5a Add blackbird media player component (#13549) 2018-04-19 11:35:38 +02:00
koolsb 3dc70436f1 Add additional receiver for Onkyo zone 2 (#13551) 2018-04-19 11:31:50 +02:00
Sebastian Muszynski 674682e88f Support for multiple MAX!Cube LAN gateways added (#13517) 2018-04-19 09:11:38 +02:00
thelittlefireman ba7fccba34 Bump locationsharinglib to 1.2.1 (#13980)
* Bump locationsharinglib to 1.2.1

*  Bump locationsharinglib to 1.2.1
2018-04-18 15:59:48 -04:00
Adam Mills ccba858ae1 Fix for Lokalise backend misinterpretation of keys (#13986)
The Lokalise server has a bug that the internal portion of key
references was misinterpreted as a symfony key, and was getting auto
converted by the convert placeholders feature. Since we don't use this
we're turning it off to work around the bug.
2018-04-18 15:58:47 -04:00
Paulus Schoutsen b0a3d084fb Version bump to 20180418.0 2018-04-18 15:58:14 -04:00
NovapaX 45eb611007 renaming icons (#13982)
* renaming icons

* remove mdi:robot-vacuum

* fix other vacuums
2018-04-18 15:46:44 -04:00
Michael Wei 0eb3e49880 Alexa thermostat fails to properly parse 'value' field for climate (#13958)
* Fix thermostat payload issue

* fix test payload

* style issue

* handle both string and value object
2018-04-18 14:19:05 -04:00
Kane610 c5cb28d41f deCONZ migrate setup fully to config entry (#13679)
* Initial working config entry with discovery

* No need for else

* Make sure that imported config doesnt exist as a config entry

* Improve checks to make sure there is only instance of deconz

* Fix tests and add new tests

* Follow upstream changes
Fix case when discovery started ongoing config entry and user completes setup  from other path it was possible to complete discovered config entry as well

* Add test to make sure link doesn't bypass any check for only allowing one config entry

* Dont use len to determine an empty sequence

* Cleanup

* Allways get bridgeid to use as unique identifier for bridge
2018-04-18 10:27:44 -04:00
Ben Randall 7d43ad6a37 Colorlog windows fix (#13929)
* Fix colorlog on windows

Modified the way logging is initialized to fix two things.
1. If the import of `colorlog` fails the logs will still be formatted
   using the expected HASS log format.
2. Ensure that `logging.basicConfig` is called AFTER `colorlog` is
   imported so that the default handler generated will be writing to the
   wrapped stream generated when `colorama` is initialized.  This allows
   colored logging to work on Windows.

Added support for a `--log-no-color` command line switch in the event
that someone just wants to disable colored log output entirely.

* Fix line lengths

* Switch default value
2018-04-18 10:18:44 -04:00
Nick Whyte b589dbf26c Support basic covers with open/close/stop services HomeKit (#13819)
* Support basic covers with open/close/stop services
* Support optional stop
* Tests
2018-04-18 14:39:58 +02:00
Sebastian Muszynski 23b97b9105 Params of the send command can be a list now (#13905) 2018-04-18 14:38:44 +02:00
stephanerosi f11d4319d2 Fix typo an coding style (#13970) 2018-04-18 12:43:55 +02:00
Mister Wil 4ba58d0760 Bump skybellpy version to 0.1.2 (#13974) 2018-04-18 10:10:32 +02:00
Paulus Schoutsen 2cb9e2dc7c Merge pull request #13975 from home-assistant/rc
0.67.1
2018-04-17 23:19:45 -04:00
Paulus Schoutsen c076dbe7e4 Revert "Upgrade pyqwikswitch to 0.71 (#13920)"
This reverts commit 6fa60c464b.
2018-04-17 22:59:36 -04:00
Paulus Schoutsen e7aea5c571 Version bump to 0.67.1 2018-04-17 22:37:40 -04:00
Aaron Bach 24ec8c545b Bumped pypollencom to 1.1.2 (#13959)
* Bumped pypollencom to 1.1.2

* Updated requirements_all.txt
2018-04-17 22:37:25 -04:00
Thibault Cohen 6c456ade6a Update pyfido to 2.1.1 (#13947) 2018-04-17 22:37:24 -04:00
Thibault Cohen e9b997de3e Update pyhydroquebec to 2.2.2 (#13946) 2018-04-17 22:37:24 -04:00
Paulus Schoutsen 53506821d4 Upgrade somecomfort to 0.5.2 (#13940) 2018-04-17 22:37:23 -04:00
Johann Kellerman 6fa60c464b Upgrade pyqwikswitch to 0.71 (#13920) 2018-04-17 22:36:07 -04:00
Sebastian Muszynski fadff1855f Import operation modes from air humidifier (#13908) 2018-04-17 22:34:40 -04:00
Daniel Høyer Iversen 652063537b Fix call to parent broadlink switch (#13906)
* Broadlink switch, fixes issue #13799

* slugify
2018-04-17 22:34:40 -04:00
Sebastian Muszynski bcd8a69dfc Missing property decorator added (#13889) 2018-04-17 22:34:40 -04:00
Paulus Schoutsen 663aeb11dc Fix race condition for component loaded before listening (#13887)
* Fix race condition for component loaded before listening

* async/await syntax
2018-04-17 22:34:39 -04:00
Kyle Niewiada 727ab956cf Fix #13846 Double underscore in bluetooth address (#13884) 2018-04-17 22:34:39 -04:00
Paulus Schoutsen 26c76e3399 Prevent vesync doing I/O in event loop (#13862) 2018-04-17 22:34:39 -04:00
Kane610 0adb240fd6 Fix so it is possible to ignore discovered config entry handlers (#13741)
* Fix so it is possible to ignore discovered config entry handlers

* Improve efficiency
2018-04-17 22:34:38 -04:00
David Broadfoot e836674a30 Fix Gogogate2 'available' attribute (#13728)
* Fixed bug -  unable to set base readaonly property

* PR fixes

* Added line
2018-04-17 22:34:38 -04:00
Aaron Bach 65b8f9764a Bumped pypollencom to 1.1.2 (#13959)
* Bumped pypollencom to 1.1.2

* Updated requirements_all.txt
2018-04-17 20:03:22 +02:00
Kane610 1a9ea11665 Bump deCONZ requirement to v36 (#13960) 2018-04-17 20:00:53 +02:00
Daniel Høyer Iversen 08f545d67b Fix call to parent broadlink switch (#13906)
* Broadlink switch, fixes issue #13799

* slugify
2018-04-17 17:40:52 +02:00
ChristianKuehnel e472436b84 Add services for bmw_connected_drive (#13497)
* implemented services for bmw remote services

* added vin to attributes of tracker
* moved component to new package
* added service description

* fixed static analysis warnings

* implemented first set of code reviews

* removed locking related services

* fixed static analysis warnings

* removed excess blank lines

* refactoring of setup() to resolve warning
"Cell variable bimmer defined in loop (cell-var-from-loop)"

* added missing docstring

* added service to update all vehicles from the server

* implemented changes requested in code review

* added check if invalid vin is entered
2018-04-17 17:37:00 +02:00
Paulus Schoutsen 783e9a5f8c Update frontend to 20180417 2018-04-17 10:17:58 -04:00
Tod Schmidt f4b1a8e42d Added web view for TTS to get url (#13882)
* Added web view for to get url

* Added web view for TTS to get url

* Added web view for TTS to get url

* Added web view for TTS to get url

* Fixed test

* added auth

* Update __init__.py
2018-04-17 15:24:54 +02:00
Dmitry Avramenko 3b44f91395 Added FB messenger broadcast api to notify.facebook component (#12459)
* Added ability to use FB messenger broadcast api. use 'BROADCAST' keyword for first target in the facebook notifiy component to enable.

* Added ability to use FB messenger broadcast api. use 'BROADCAST' keyword for first target in the facebook notifiy component to enable.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Added ability for broadcast messaging for facebook messenger notify platform.

* Update facebook.py

* Update facebook.py

* Update facebook.py

* Update facebook.py
2018-04-17 14:23:41 +02:00
Fabian Affolter cff3bed1f0 Upgrade youtube_dl to 2018.04.16 (#13937) 2018-04-17 13:32:44 +02:00
Fabian Affolter 9fe43714c6 Upgrade aiohttp to 3.1.3 (#13938) 2018-04-17 13:32:16 +02:00
Robin 569f5c111f Adds SigFox sensor (#13731)
* Create sigfox.py

* Create test_sigfox.py

* Update .coveragerc

* Fix lints

* Fix logger message string

* More lints

* Address reviewer comments

* edit exception handling

* Update sigfox.py

* Update sigfox.py

* Update sigfox.py

* Update sigfox.py
2018-04-17 13:08:32 +02:00
Heiko Thiery 9487bd455a Add AVM fritzbox smarthome component (#10688)
* initial commit

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

* fix failed flake8 tests

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

* add fritzhome files to .coveragerc

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

* fix wrong module import

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

* remove too general exception

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

* incorporate review comments

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

* remove blank line

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

* fix wrong import

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

* fix issue with operations

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

* incorporate review comments

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

* remove unused attributes

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

* adapt to supported_features

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

* change checking of kwargs to canonical way

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

* remove unused self._state

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

* Don't overwrite the platform domain

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

* Remove parenthesis from import without line break

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

* Do not pass hass to the components on init

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

* Remove check for available in current_operation

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

* Remove redundant logging message

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

* Add blank line between standard and hass imports

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

* Use states from base climate component

Also add the new state STATE_MANUAL to the base.

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

* add reconnect when access failed

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

* add device specific attributes

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

* group the imports from the same module

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

* change domain data to fritz instance

This let us use the fritz instance to reconnect from platform without accessing
protected attributes.

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

* fix typo

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

* rename platform from fritzhome to fritzbox

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

* Add device_state_attributes

Add attributes to have compatiblity to fritzdect.

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

* add support for multiple fritzboxes

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

* fix pylint issues

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

* fixed pyfritzhome version

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

* fix import

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

* fix component name in requirements_all.txt

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

* upgrade pyfritzhome to 0.3.7

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

* rename platform/component also in .coveragerc

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

* use DEFAULT_HOST when no host is in dict

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

* add config schema for dict

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

* remove check

The check since since the config scheme takes case.

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

* add check for empty devices

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

* use standard attribute from base class

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

* remove STATE_MANUAL from operation list

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

* remove set DEFAULT_HOST

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

* don't pass hass to the SwitchDevice

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

* remove unsed DEFAULT_HOST

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

* refactored device attributes

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

* add info output if no fritzbox is configured

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

* small fixes according review comment

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

* remove unneeded default value

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

* remove non required code from try..except block

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

* line break for line that is too long

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

* remove too many empty lines

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>
2018-04-17 12:40:36 +02:00
karlkar f2d4dd25f0 Update of python-mpd2 (#13921) 2018-04-17 11:55:35 +02:00
stephanerosi 998d8c1771 Implement play media to set a channel based on (by priority): (#13934)
- exact channel number
 - exact channel name
 - similar channel name temp
2018-04-17 11:50:26 +02:00
Sebastian Muszynski add0afe31a Xiaomi MiIO Device Tracker: Unused variable removed (#13948)
* Unused variable removed and pinning added to be in sync with all xiaomi_miio components

* requirements_all.txt updated
2018-04-17 11:45:19 +02:00
Paulus Schoutsen 534aa0e4b5 Add data entry flow helper (#13935)
* Extract data entry flows HTTP views into helper

* Remove use of domain

* Lint

* Fix tests

* Update doc
2018-04-17 11:44:32 +02:00
Paulus Schoutsen 6e9669c18d Upgrade somecomfort to 0.5.2 (#13940) 2018-04-17 05:24:20 +02:00
Diogo Gomes 8fdeebc50d Cleanup on exit (#13918)
* Cleanup on exit

* lint

* version bump

* pymediaroom version bump

* address @kellerza comment

* avoid None in the _name
2018-04-16 22:21:39 -04:00
Thibault Cohen d0d61d1b5f Update pyfido to 2.1.1 (#13947) 2018-04-16 22:16:28 -04:00
Fabian Affolter e8ad36feb6 Upgrade alpha_vantage to 2.0.0 (#13943) 2018-04-16 22:16:12 -04:00
Thibault Cohen 9da239178c Update pyhydroquebec to 2.2.2 (#13946) 2018-04-17 02:52:56 +02:00
Fabien Piuzzi acdba7a27c Updated foobot_async package version (#13942)
Fix #13886
2018-04-16 21:35:24 +02:00
Khole e0c5b44994 Hive R3 update (#13357)
* Rebase

* Update version number to 0.2.14

* Remove Blank Line

* Added period to docstring

* Update Tox Fix

* Removed Lines
2018-04-16 21:00:13 +02:00
Lincoln Kirchoff 595600dea5 Add support for new platform: climate.modbus (#12224)
* Added support for a new platform: climate.modbus

* Made changes based on code review.

* Made changes based on code review

* Made changes that were recommended in the pull request review.

* Fixed spacing line 144

* Added docstrings for the added helper functions.

* Fixed set_temperature() function to use a variable local to the function for the target temp.

* Fixed lint formatting error

* Modified logic when checking the target temperature, as well as fixing the setup_platform function
2018-04-16 20:31:25 +02:00
Paxy ad212d8dd4 Broadlink Sensor - switch to connection-less mode (#13761)
* Broadlink Sensor - switch to connection-less mode

Solved the issue with broadlink sensor that occurs when short connection loss with RM2/3 is present on poor WiFi networks.

* Update broadlink.py

* Update broadlink.py

* Update broadlink.py

* Update broadlink.py

* Update broadlink.py

* Update broadlink.py

* Update broadlink.py
2018-04-16 12:06:41 +02:00
Marco 86709427b6 Fixed Capsman data not being used (#13917) 2018-04-16 09:54:57 +02:00
stephanerosi 36a663adeb Add extra attributes for device scanner, Nmap and Unifi (IP, SSID, etc.) (#13673)
* Start of development

* Add extra attributes from unifi scanner

* Store IP of the device in the state attributes with nmap

* Allow not defining get_extra_attributes method in derived classes
2018-04-16 08:20:58 +02:00
Johann Kellerman 517fb2e983 Upgrade pyqwikswitch to 0.71 (#13920) 2018-04-15 22:19:15 +02:00
Benedict Aas 9677bc081e Add more math functions to templates (#13915)
We make `sin`, `cos`, `tan`, and `sqrt` functions, and the `pi`, `tau`,
and `e` constants available in templates.
2018-04-15 18:51:45 +02:00
Josh Anderson c69f37500a Restore typeerror check for units sans energy tracking (#13824) 2018-04-15 15:25:30 +02:00
escoand cd8935cbd2 Fritzbox netmonitor name (#13903)
* Addd name to netmonitor

* import conf_name
2018-04-15 15:20:37 +02:00
Sebastian Muszynski 2f26b0084f Import operation modes from air humidifier (#13908) 2018-04-15 15:19:28 +02:00
Kyle Niewiada 2bff03836b Fix #13846 Double underscore in bluetooth address (#13884) 2018-04-15 13:59:10 +02:00
Matthew Garrett 390086bb7e Eufy colour bulb updates (#13895)
* Fix up Eufy handling of colour lights

The Eufy colour lights have separate colour and temperature modes, and give
much less light output when in colour mode. Brightness is also handled in
a slightly confusing way, which means that state must be maintained in
order to avoid switching the light between modes by accident. Add some
additional handling for that.

* Bump the lakeside version

This version has important bugfixes for colour bulbs.

* Hound fixes
2018-04-15 09:54:02 +02:00
Pascal Vizeli c018071218 Revert "Update yweather.py" (#13900)
* Revert "Add unique_id for BMW ConnectedDrive (#13888)"

This reverts commit 9014e26845.

* Revert "Added snips service descriptions (#13883)"

This reverts commit 1c4da0c4a6.

* Revert "Fix race condition for component loaded before listening (#13887)"

This reverts commit bba997e484.

* Revert "Missing property decorator added (#13889)"

This reverts commit bf98b793c5.

* Revert "Update frontend to 20180414.0"

This reverts commit 1617fbea4c.

* Revert "Further untangle data entry flow (#13855)"

This reverts commit 4d44c0feff.

* Revert "add support for Kodi discovery (#13790)"

This reverts commit 5a5dad689b.

* Revert "Update yweather.py (#13851)"

This reverts commit c3388d63a1.
2018-04-15 09:50:44 +02:00
Gerard 9014e26845 Add unique_id for BMW ConnectedDrive (#13888)
* Add unique_id for BMW ConnectedDrive

* Changed some comments
2018-04-15 05:15:52 +02:00
Tod Schmidt 1c4da0c4a6 Added snips service descriptions (#13883)
* Added snips service descriptions.

* Added snips service descriptions.
2018-04-15 00:07:55 +02:00
Paulus Schoutsen bba997e484 Fix race condition for component loaded before listening (#13887)
* Fix race condition for component loaded before listening

* async/await syntax
2018-04-14 17:58:45 -04:00
Sebastian Muszynski bf98b793c5 Missing property decorator added (#13889) 2018-04-14 23:53:35 +02:00
Paulus Schoutsen 1617fbea4c Update frontend to 20180414.0 2018-04-14 14:41:21 -04:00
Paulus Schoutsen 4d44c0feff Further untangle data entry flow (#13855)
* Further untangle data entry flow

* Fix test

* Remove helper class
2018-04-14 14:38:24 -04:00
escoand 5a5dad689b add support for Kodi discovery (#13790)
* add support for Kodi discovery

* remove "too many blank lines"

* register service only once

* optimize "workflow"
2018-04-14 08:31:12 -04:00
TheCellMC c3388d63a1 Update yweather.py (#13851)
* Update yweather.py

* Update yweather.py

* Update yweather.py

* Update yweather.py
2018-04-14 10:32:44 +02:00
Paulus Schoutsen ee6acadae2 Prevent vesync doing I/O in event loop (#13862) 2018-04-14 10:31:03 +02:00
dersger 80a3220b88 Avoid unnecessary cast state updates (#13770)
* Avoid unnecessary cast state updates

* Add test

* Fixed bad syntax

* Fixed imports

* Fixed test
2018-04-13 22:22:02 -04:00
Mohamad Tarbin 99ded8a0a6 Adding USCIS component (#13764)
* Adding USCIS component

* Adding Line after the class DOC

* Update : Extract USCIS logic code to Component

* Update : Extract USCIS logic code to Component

* Adding CURRENT_STATUS

* Change Error handling, remove date from attributes

* Update the Version for USCIS

* Update uscis.py
2018-04-13 21:54:23 -04:00
geekofweek c6c166645d bump python-ecobee-api version to 0.0.18 (#13854)
* bump python-ecobee-api version to 0.0.18

* Update requirements_all.txt
2018-04-13 21:36:46 -04:00
Paulus Schoutsen 0daf38d18c Version bump to 0.68.0.dev0 2018-04-13 18:02:51 -04:00
Paulus Schoutsen 5ec30ce1e6 Merge branch 'master' into dev 2018-04-13 18:02:15 -04:00
Paulus Schoutsen fb91b05051 Merge pull request #13856 from home-assistant/rc
0.67.0
2018-04-13 17:59:39 -04:00
Paulus Schoutsen c36c2be372 Version bump to 0.67.0 2018-04-13 16:52:50 -04:00
Paulus Schoutsen 598f093bf0 Add authentication to error log endpoint (#13836) 2018-04-13 16:52:22 -04:00
stephanerosi b9306a5e52 Channel up/down for LiveTV and next/previous for other apps (#13829) 2018-04-13 16:52:21 -04:00
Matthew Garrett ac2298189e Add support for controlling homekit lights and switches (#13346)
* Add support for controlling homekit lights and switches

This adds support for controlling lights and switches that expose a HomeKit
control interface, avoiding the requirement to implement protocol-specific
components.

* Comment out the homekit requirement

This needs to build native code, so leave it commented for now

* Review updates

* Make HomeKit auto-discovery optional

Add an "enable" argument to the discovery component and add a list of
optional devices types (currently just HomeKit) to discover

* Further review comments

* Update requirements_all.txt

* Fix houndci complaints

* Further review updates

* Final review fixup

* Lint fixups

* Fix discovery tests

* Further review updates
2018-04-13 19:25:35 +02:00
Paulus Schoutsen 60508f7215 Extract config flow to own module (#13840)
* Extract config flow to own module

* Lint

* fix lint

* fix typo

* ConfigFlowHandler -> FlowHandler

* Rename to data_entry_flow
2018-04-13 10:14:53 -04:00
Diogo Gomes ddd2003629 initialize queue before filtering (#13842) 2018-04-13 14:25:03 +02:00
Paulus Schoutsen 20ababec3e Add authentication to error log endpoint (#13836) 2018-04-13 13:32:05 +02:00
Mark Coombes d3b261a25d Add support for deCONZ daylight sensor (#13479)
* Add support for deCONZ daylight sensor

Bump pydeconz to 34

* Remove 'daylight' reason from async u
2018-04-13 08:58:57 +02:00
Fabian Affolter 3906250c9e Update example (fixes #13834) (#13839) 2018-04-13 08:50:58 +02:00
Diogo Gomes 22a1b99e57 UPnP async (#13666)
* moved from miniupnpc to pyupnp-async

* update requirements

* Tests added

* hound

* update requirements_test_all.txt

* update gen_requirements_all.py

* addresses @pvizeli requested changes

* address review comments
2018-04-13 00:22:52 +02:00
Mister Wil 62dc737ea3 Abode better events (#13809)
* Push abodepy version to 0.13.0

* Bump to 0.13.1. Now uses a cache to store the generated UUID.

* Reorganize to not be a dumb dumb.
2018-04-12 22:27:23 +02:00
Mark Coombes 993866a314 Support Garage Doors in HomeKit (#13796) 2018-04-12 18:08:48 +02:00
xTCx 51bdd06d1f Clicksend: Added support for multiple recipients (#13812)
* Clicksend: Added support for multiple recipients

* Removed whitespace
2018-04-12 16:13:31 +02:00
stephanerosi d2804b0a27 Channel up/down for LiveTV and next/previous for other apps (#13829) 2018-04-12 15:44:56 +02:00
Yonsm c863b9614c Support CO2/PM2.5/Light sensors in HomeKit (#13804)
* Support co2/light/air sensor in HomeKit
* Add tests
* Added tests
* changed device_class lux to light
2018-04-12 15:01:41 +02:00
Paulus Schoutsen f47572d3c0 Allow platform unloading (#13784)
* Allow platform unloading

* Add tests

* Add last test
2018-04-12 14:28:54 +02:00
Paulus Schoutsen 9bd29589d5 Version bump to 0.67.0b1 2018-04-12 08:22:07 -04:00
Marco Orovecchia f29904f1b5 Rename from aurora light to nanoleaf_aurora (#13831) 2018-04-12 08:21:52 -04:00
Anders Melchiorsen 234495ed05 Fix too green color conversion (#13828)
* Prepare test

* Fix too green color conversion

* Fix remaining tests
2018-04-12 08:21:52 -04:00
Adam Mills 09dbd94467 iglo hs color fix (#13808) 2018-04-12 08:21:51 -04:00
Paulus Schoutsen bd58a0de7d Remove vendor lookup for mac addresses (#13788)
* Remove vendor lookup for mac addresses

* Fix tests
2018-04-12 08:21:51 -04:00
cdce8p dd7e6edf61 HomeKit type_cover fix (#13832)
* Removed char_position_state
* Changed service call
2018-04-12 13:19:21 +02:00
Marco Orovecchia b752ca3bef Rename from aurora light to nanoleaf_aurora (#13831) 2018-04-12 09:24:07 +02:00
Anders Melchiorsen 9c1bc18def Fix too green color conversion (#13828)
* Prepare test

* Fix too green color conversion

* Fix remaining tests
2018-04-11 20:58:57 -04:00
cdce8p 2a5751c09d Homekit refactor (#13707) 2018-04-11 22:24:14 +02:00
Matthew Garrett 8d48164f25 Add support for Eufy bulbs and switches (#13773)
* Add support for Eufy bulbs and switches

Add support for driving bulbs and switches from the Eufy range.

* Fix hound checks

* Satisfy pylint

* Handle review comments

* Review updates and test fixes

* PyLint is a bit too aggressive
2018-04-10 21:38:23 -04:00
Daniel Perna b2695e498d Update pyhomematic to 0.1.41 (#13814)
* Update requirements_all.txt

* Update __init__.py
2018-04-10 23:33:56 +02:00
Daniel Høyer Iversen 16a1a4e0b1 Tibber lib update (#13811) 2018-04-10 22:12:55 +02:00
Wojtek 191e32f6cf Update yweather.py (#13802)
Map clear-night string to 31 value.
2018-04-10 21:11:45 +02:00
Toby Gray 978a79d369 device_tracker.ubus: Handle devices not running DHCP (#13579) 2018-04-10 20:38:36 +02:00
Adam Mills cf88d8a1b9 iglo hs color fix (#13808) 2018-04-10 14:11:00 -04:00
Russell Cloran 2707d35a86 Update bellows to 0.5.2 (#13800) 2018-04-10 00:12:22 -07:00
Michael Kutý 7ea776dff4 Fix bad metrics format for short metrics. (#13778) 2018-04-10 08:20:47 +02:00
Johann Kellerman bd93f10d3c script/lazytox: Ensure Flake8 passes for tests/ (#13794) 2018-04-09 21:24:50 -04:00
citruz c8a464d8f9 Updated beacontools to 1.2.3 (#13792) 2018-04-09 21:24:18 -04:00
Paulus Schoutsen 5ac52b74e0 Remove vendor lookup for mac addresses (#13788)
* Remove vendor lookup for mac addresses

* Fix tests
2018-04-09 21:21:26 -04:00
Johann Kellerman 7595401dcb Qwikswitch Entity Register (#13791)
* Entity Register

* feedback
2018-04-10 01:24:06 +02:00
cdce8p ae4e792651 Improved upgradeability HomeKit security_systems (#13783) 2018-04-09 22:57:10 +02:00
Sean Wilson 2b86059fd0 Add missing DISCHRG state (#13787)
* Add missing ups.status states.

* Add missing DISCHRG state.
2018-04-09 19:38:57 +02:00
Tod Schmidt e593117ab6 Snips sounds (#13746)
* Added feedback sound configuration

* Added feedback sound configuration

* Cleaned up feedback off

* Cleaned up whitespace

* Moved feedback pus to helper funx

* Async

* Used async_mock_service for tests

* Lint
2018-04-09 11:46:27 -04:00
Phil Kates c61611d2b4 Add Homekit locks support (#13625)
* homekit: Add locks support
* Improved upgradeability
2018-04-09 16:23:49 +02:00
Paulus Schoutsen 73de749411 Use config entry to setup platforms (#13752)
* Use config entry to setup platforms

* Rename to async_forward_entry

* Add tests

* Catch if platform not exists for entry
2018-04-09 10:09:08 -04:00
Yonsm cb51553c2d Support binary_sensor and device_tracker in HomeKit (#13735)
* Support binary_sensor and device_tracker for HomeKit
* Add test for get_accessory and binary sensor
* Test service.display_name and char_detected.display_name
* Split test to improve speed
2018-04-09 15:32:28 +02:00
Erik Eriksson 8beb9c2b28 Only flag media position as updated when it really has (#13737) 2018-04-09 00:12:46 -04:00
Sebastian Muszynski 70649dfe22 Device type mapping introduced to avoid breaking change (#13765) 2018-04-08 22:00:47 +02:00
Johann Kellerman b01dceaff2 Qwikswitch sensors (#13622) 2018-04-08 21:59:19 +02:00
Robin ef16c53e46 Check valid file on get_size (#13756)
Addresses https://github.com/home-assistant/home-assistant/issues/13754
2018-04-08 11:32:49 +02:00
Paulus Schoutsen 40d7857f3b Prepare entity component for config entries (#13730)
* Prepare entity component for config entries

* Return in time
2018-04-07 23:04:50 -04:00
Otto Winter 81b1d08d35 Add MQTT Sensor unique_id (#13318)
* Add MQTT Sensor unique_id

* Add test

* Update comment
2018-04-07 22:32:09 -04:00
Fabian Affolter 99f4509c2b Upgrade netdisco to 1.3.1 (#13744) 2018-04-07 17:19:55 -04:00
Kane610 f915a1c809 Fix so it is possible to ignore discovered config entry handlers (#13741)
* Fix so it is possible to ignore discovered config entry handlers

* Improve efficiency
2018-04-07 17:18:49 -04:00
dangyuluo 6cd599b7df Throw an error when invalid device_mode is given (#13739)
* Throw an error when invalid device_mode is given

* Fix lint issue, typo and error msg

* Fix error msg
2018-04-07 21:47:56 +02:00
Fabian Affolter 435b49fb96 Reset permission (#13743) 2018-04-07 19:11:48 +02:00
Diogo Gomes 3084ac1625 Update CODEOWNERS (sensor.filter, sensor.upnp) (#13736) 2018-04-07 13:44:08 +02:00
shred86 2bf17cba8e Brightness conversion for Abode dimmers (#13711)
With AbodePy 0.12.3, dimmers will now work but a conversion of the brightness is required. Additionally, when a brightness value of 100 is sent to Abode, 99 is returned causing AbodePy to throw an error so this component will send 99 instead of 100.

Keeps the brightness value sent and returned from the device response consistent. However, during initialization and when a device refresh is received, Abode can return 100 thus we'll convert that case back to 99.
2018-04-07 11:15:35 +02:00
Fabian Affolter ca3cc27e40 Upgrade sqlalchemy to 1.2.6 (#13733) 2018-04-07 10:41:35 +02:00
Fabian Affolter fbb8a54c39 Upgrade aiohttp to 3.1.2 (#13732) 2018-04-07 10:40:34 +02:00
thrawnarn b0fd2342db Bluesound bugfix status 595 and await (#13727)
* 595 fix

* Await fixes and last 595 fix

* Lint

* Made internal exception class

* Fix lint issue
2018-04-07 10:09:09 +02:00
David Broadfoot 58f3690ef6 Fix Gogogate2 'available' attribute (#13728)
* Fixed bug -  unable to set base readaonly property

* PR fixes

* Added line
2018-04-07 05:48:53 +02:00
Diogo Gomes 286476f0d6 Initialise filter_sensor with historical values (#13075)
* Initialise filter with historical values
Added get_last_state_changes()

* fix test

* Major changes to accommodate history + time_SMA

# Conflicts:
#	homeassistant/components/sensor/filter.py

* hail the hound!

* lint fixed

* less debug

* ups

* get state from the proper entity

* sensible default

* No defaults in get_last_state_changes

* list_reverseiterator instead of list

* prev_state to state

* Initialise filter with historical values
Added get_last_state_changes()

* fix test

* Major changes to accommodate history + time_SMA

# Conflicts:
#	homeassistant/components/sensor/filter.py

* hail the hound!

* lint fixed

* less debug

* ups

* get state from the proper entity

* sensible default

* No defaults in get_last_state_changes

* list_reverseiterator instead of list

* prev_state to state

* update

* added window_unit

* replace isinstance with window_unit
2018-04-06 21:59:55 -04:00
Henrik Nicolaisen fdf93d1829 added support for smappee water sensors (#12831)
* added support for smappee water sensors

* fixed lint error and wrong location_id

* fixed lint error

* Use string formatting
2018-04-06 23:14:31 +02:00
cdce8p 262ea14e5a Add timeout / debounce (for brightness and others) (#13534)
* Add async timeout feature

* Decorator for setter methods to limit service calls to HA
* Changed to async
* Use async_call_later
* Use lastargs, async_add_job

* Use dict for lastargs

* Updated tests to stop patch
2018-04-06 23:11:53 +02:00
Juggels c77d013f43 Allow use of date_string in service call (#13256)
* Allow use of date_string in service call

* Add stricter validation, fix descriptions
2018-04-06 22:23:40 +02:00
cgtobi 48fe2d18e8 Add option to ignore availability in google calendar events (#13714) 2018-04-06 21:48:50 +02:00
Fabian Affolter 3394916a68 Update docstrings (#13720) 2018-04-06 18:06:47 +02:00
cdce8p 85487612d5 Update Homekit to 1.1.9 (#13716)
* Version bump to HAP-python==1.1.9

* Updated types and tests
2018-04-06 10:20:59 -04:00
223 changed files with 8129 additions and 2584 deletions
+15 -3
View File
@@ -94,6 +94,12 @@ omit =
homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py
homeassistant/components/fritzbox.py
homeassistant/components/*/fritzbox.py
homeassistant/components/eufy.py
homeassistant/components/*/eufy.py
homeassistant/components/gc100.py
homeassistant/components/*/gc100.py
@@ -106,6 +112,9 @@ omit =
homeassistant/components/hive.py
homeassistant/components/*/hive.py
homeassistant/components/homekit_controller/__init__.py
homeassistant/components/*/homekit_controller.py
homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py
@@ -190,8 +199,8 @@ omit =
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py
homeassistant/components/switch/qwikswitch.py
homeassistant/components/light/qwikswitch.py
homeassistant/components/rachio.py
homeassistant/components/*/rachio.py
@@ -407,7 +416,6 @@ omit =
homeassistant/components/image_processing/seven_segments.py
homeassistant/components/keyboard_remote.py
homeassistant/components/keyboard.py
homeassistant/components/light/aurora.py
homeassistant/components/light/avion.py
homeassistant/components/light/blinksticklight.py
homeassistant/components/light/blinkt.py
@@ -422,6 +430,7 @@ omit =
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py
homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/osramlightify.py
homeassistant/components/light/piglow.py
homeassistant/components/light/rpi_gpio_pwm.py
@@ -639,7 +648,9 @@ omit =
homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/sht31.py
homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/sigfox.py
homeassistant/components/sensor/simulated.py
homeassistant/components/sensor/skybeacon.py
homeassistant/components/sensor/sma.py
@@ -669,6 +680,7 @@ omit =
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py
homeassistant/components/sensor/uscis.py
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py
+1 -1
View File
@@ -31,7 +31,7 @@ script: travis_wait 30 tox --develop
services:
- docker
before_deploy:
- docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7
- docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
deploy:
skip_cleanup: true
provider: script
+2
View File
@@ -63,6 +63,7 @@ homeassistant/components/media_player/xiaomi_tv.py @fattdev
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
@@ -72,6 +73,7 @@ homeassistant/components/sensor/sma.py @kellerza
homeassistant/components/sensor/sql.py @dgomes
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/upnp.py @dgomes
homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti
+7 -2
View File
@@ -126,6 +126,10 @@ def get_arguments() -> argparse.Namespace:
default=None,
help='Log file to write to. If not set, CONFIG/home-assistant.log '
'is used')
parser.add_argument(
'--log-no-color',
action='store_true',
help="Disable color logs")
parser.add_argument(
'--runner',
action='store_true',
@@ -259,13 +263,14 @@ def setup_and_run_hass(config_dir: str,
hass = bootstrap.from_config_dict(
config, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
log_file=args.log_file)
log_file=args.log_file, log_no_color=args.log_no_color)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
hass = bootstrap.from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days, log_file=args.log_file)
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
log_no_color=args.log_no_color)
if hass is None:
return None
+47 -29
View File
@@ -42,7 +42,8 @@ def from_config_dict(config: Dict[str, Any],
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None) \
log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@@ -60,7 +61,7 @@ def from_config_dict(config: Dict[str, Any],
hass = hass.loop.run_until_complete(
async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days, log_file)
log_rotate_days, log_file, log_no_color)
)
return hass
@@ -74,7 +75,8 @@ def async_from_config_dict(config: Dict[str, Any],
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None) \
log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@@ -84,7 +86,8 @@ def async_from_config_dict(config: Dict[str, Any],
start = time()
if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
core_config = config.get(core.DOMAIN, {})
@@ -164,7 +167,8 @@ def from_config_file(config_path: str,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None):
log_file: Any = None,
log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@@ -176,7 +180,8 @@ def from_config_file(config_path: str,
# run task
hass = hass.loop.run_until_complete(
async_from_config_file(
config_path, hass, verbose, skip_pip, log_rotate_days, log_file)
config_path, hass, verbose, skip_pip,
log_rotate_days, log_file, log_no_color)
)
return hass
@@ -188,7 +193,8 @@ def async_from_config_file(config_path: str,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None):
log_file: Any = None,
log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
@@ -199,7 +205,8 @@ def async_from_config_file(config_path: str,
hass.config.config_dir = config_dir
yield from async_mount_local_lib_path(config_dir, hass.loop)
async_enable_logging(hass, verbose, log_rotate_days, log_file)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
try:
config_dict = yield from hass.async_add_job(
@@ -216,40 +223,51 @@ def async_from_config_file(config_path: str,
@core.callback
def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
log_rotate_days=None, log_file=None) -> None:
def async_enable_logging(hass: core.HomeAssistant,
verbose: bool = False,
log_rotate_days=None,
log_file=None,
log_no_color: bool = False) -> None:
"""Set up the logging.
This method must be run in the event loop.
"""
logging.basicConfig(level=logging.INFO)
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s")
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
datefmt = '%Y-%m-%d %H:%M:%S'
if not log_no_color:
try:
from colorlog import ColoredFormatter
# basicConfig must be called after importing colorlog in order to
# ensure that the handlers it sets up wraps the correct streams.
logging.basicConfig(level=logging.INFO)
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
# If the above initialization failed for any reason, setup the default
# formatting. If the above succeeds, this wil result in a no-op.
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
# Log errors to a file if we have write access to file or config dir
if log_file is None:
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
+7 -4
View File
@@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['abodepy==0.12.3']
REQUIREMENTS = ['abodepy==0.13.1']
_LOGGER = logging.getLogger(__name__)
@@ -27,6 +27,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com"
CONF_POLLING = 'polling'
DOMAIN = 'abode'
DEFAULT_CACHEDB = './abodepy_cache.pickle'
NOTIFICATION_ID = 'abode_notification'
NOTIFICATION_TITLE = 'Abode Security Setup'
@@ -87,12 +88,13 @@ ABODE_PLATFORMS = [
class AbodeSystem(object):
"""Abode System class."""
def __init__(self, username, password, name, polling, exclude, lights):
def __init__(self, username, password, cache,
name, polling, exclude, lights):
"""Initialize the system."""
import abodepy
self.abode = abodepy.Abode(
username, password, auto_login=True, get_devices=True,
get_automations=True)
get_automations=True, cache_path=cache)
self.name = name
self.polling = polling
self.exclude = exclude
@@ -129,8 +131,9 @@ def setup(hass, config):
lights = conf.get(CONF_LIGHTS)
try:
cache = hass.config.path(DEFAULT_CACHEDB)
hass.data[DOMAIN] = AbodeSystem(
username, password, name, polling, exclude, lights)
username, password, cache, name, polling, exclude, lights)
except (AbodeException, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
@@ -1471,6 +1471,7 @@ async def async_api_adjust_target_temp(hass, config, request, entity):
async def async_api_set_thermostat_mode(hass, config, request, entity):
"""Process a set thermostat mode request."""
mode = request[API_PAYLOAD]['thermostatMode']
mode = mode if isinstance(mode, str) else mode['value']
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
# Work around a pylint false positive due to
+13 -3
View File
@@ -52,9 +52,8 @@ def setup(hass, config):
hass.http.register_view(APIComponentsView)
hass.http.register_view(APITemplateView)
log_path = hass.data.get(DATA_LOGGING, None)
if log_path:
hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False)
if DATA_LOGGING in hass.data:
hass.http.register_view(APIErrorLog)
return True
@@ -356,6 +355,17 @@ class APITemplateView(HomeAssistantView):
HTTP_BAD_REQUEST)
class APIErrorLog(HomeAssistantView):
"""View to fetch the error log."""
url = URL_API_ERROR_LOG
name = "api:error_log"
async def get(self, request):
"""Retrieve API error log."""
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
@asyncio.coroutine
def async_services_json(hass):
"""Generate services data to JSONify."""
@@ -46,6 +46,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name
self._device_class = device_class
self._state = None
@@ -55,6 +56,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
"""Data update is triggered from BMWConnectedDriveEntity."""
return False
@property
def unique_id(self):
"""Return the unique ID of the binary sensor."""
return self._unique_id
@property
def name(self):
"""Return the name of the binary sensor."""
@@ -32,6 +32,7 @@ class HiveBinarySensorEntity(BinarySensorDevice):
self.device_type = hivedevice["HA_DeviceType"]
self.node_device_type = hivedevice["Hive_DeviceType"]
self.session = hivesession
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
@@ -52,6 +53,11 @@ class HiveBinarySensorEntity(BinarySensorDevice):
"""Return the name of the binary sensor."""
return self.node_name
@property
def device_state_attributes(self):
"""Show Device Attributes."""
return self.attributes
@property
def is_on(self):
"""Return true if the binary sensor is on."""
@@ -61,3 +67,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
def update(self):
"""Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
self.attributes = self.session.attributes.state_attributes(
self.node_id)
@@ -7,7 +7,7 @@ https://home-assistant.io/components/maxcube/
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.maxcube import MAXCUBE_HANDLE
from homeassistant.components.maxcube import DATA_KEY
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
@@ -15,16 +15,17 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add window shutters."""
cube = hass.data[MAXCUBE_HANDLE].cube
devices = []
for handler in hass.data[DATA_KEY].values():
cube = handler.cube
for device in cube.devices:
name = "{} {}".format(
cube.room_by_id(device.room_id).name, device.name)
for device in cube.devices:
name = "{} {}".format(
cube.room_by_id(device.room_id).name, device.name)
# Only add Window Shutters
if cube.is_windowshutter(device):
devices.append(MaxCubeShutter(hass, name, device.rf_address))
# Only add Window Shutters
if cube.is_windowshutter(device):
devices.append(
MaxCubeShutter(handler, name, device.rf_address))
if devices:
add_devices(devices)
@@ -33,12 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MaxCubeShutter(BinarySensorDevice):
"""Representation of a MAX! Cube Binary Sensor device."""
def __init__(self, hass, name, rf_address):
def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube BinarySensorDevice."""
self._name = name
self._sensor_type = 'window'
self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE]
self._cubehandle = handler
self._state = STATE_UNKNOWN
@property
@@ -0,0 +1,70 @@
"""
Support for Qwikswitch Binary Sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.qwikswitch/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH
from homeassistant.core import callback
DEPENDENCIES = [QWIKSWITCH]
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, _, add_devices, discovery_info=None):
"""Add binary sensor from the main Qwikswitch component."""
if discovery_info is None:
return
qsusb = hass.data[QWIKSWITCH]
_LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s",
qsusb, discovery_info)
devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]]
add_devices(devs)
class QSBinarySensor(QSEntity, BinarySensorDevice):
"""Sensor based on a Qwikswitch relay/dimmer module."""
_val = False
def __init__(self, sensor):
"""Initialize the sensor."""
from pyqwikswitch import SENSORS
super().__init__(sensor['id'], sensor['name'])
self.channel = sensor['channel']
sensor_type = sensor['type']
self._decode, _ = SENSORS[sensor_type]
self._invert = not sensor.get('invert', False)
self._class = sensor.get('class', 'door')
@callback
def update_packet(self, packet):
"""Receive update packet from QSUSB."""
val = self._decode(packet, channel=self.channel)
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
self.entity_id, self.qsid, self.channel, val, packet)
if val is not None:
self._val = bool(val)
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Check if device is on (non-zero)."""
return self._val == self._invert
@property
def unique_id(self):
"""Return a unique identifier for this sensor."""
return "qs{}:{}".format(self.qsid, self.channel)
@property
def device_class(self):
"""Return the class of this sensor."""
return self._class
@@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
CONF_REGION = 'region'
ATTR_VIN = 'vin'
ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
@@ -35,35 +35,40 @@ CONFIG_SCHEMA = vol.Schema({
},
}, extra=vol.ALLOW_EXTRA)
SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_VIN): cv.string,
})
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes
SERVICE_UPDATE_STATE = 'update_state'
def setup(hass, config):
_SERVICE_MAP = {
'light_flash': 'trigger_remote_light_flash',
'sound_horn': 'trigger_remote_horn',
'activate_air_conditioning': 'trigger_remote_air_conditioning',
}
def setup(hass, config: dict):
"""Set up the BMW connected drive components."""
accounts = []
for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name)
bimmer = BMWConnectedDriveAccount(username, password, region, name)
accounts.append(bimmer)
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
now = datetime.datetime.now()
track_utc_time_change(
hass, bimmer.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second)
accounts.append(setup_account(account_config, hass, name))
hass.data[DOMAIN] = accounts
for account in accounts:
account.update()
def _update_all(call) -> None:
"""Update all BMW accounts."""
for cd_account in hass.data[DOMAIN]:
cd_account.update()
# Service to manually trigger updates for all accounts.
hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all)
_update_all(None)
for component in BMW_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
@@ -71,6 +76,48 @@ def setup(hass, config):
return True
def setup_account(account_config: dict, hass, name: str) \
-> 'BMWConnectedDriveAccount':
"""Set up a new BMWConnectedDriveAccount based on the config."""
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name)
cd_account = BMWConnectedDriveAccount(username, password, region, name)
def execute_service(call):
"""Execute a service for a vehicle.
This must be a member function as we need access to the cd_account
object here.
"""
vin = call.data[ATTR_VIN]
vehicle = cd_account.account.get_vehicle(vin)
if not vehicle:
_LOGGER.error('Could not find a vehicle for VIN "%s"!', vin)
return
function_name = _SERVICE_MAP[call.service]
function_call = getattr(vehicle.remote_services, function_name)
function_call()
# register the remote services
for service in _SERVICE_MAP:
hass.services.register(
DOMAIN, service,
execute_service,
schema=SERVICE_SCHEMA)
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
now = datetime.datetime.now()
track_utc_time_change(
hass, cd_account.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second)
return cd_account
class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""
@@ -0,0 +1,42 @@
# Describes the format for available services for bmw_connected_drive
#
# The services related to locking/unlocking are implemented in the lock
# component to avoid redundancy.
light_flash:
description: >
Flash the lights of the vehicle. The vehicle is identified via the vin
(see below).
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567
sound_horn:
description: >
Sound the horn of the vehicle. The vehicle is identified via the vin
(see below).
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567
activate_air_conditioning:
description: >
Start the air conditioning of the vehicle. What exactly is started here
depends on the type of vehicle. It might range from just ventilation over
auxilary heating to real air conditioning. The vehicle is identified via
the vin (see below).
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567
update_state:
description: >
Fetch the last state of the vehicles of all your accounts from the BMW
server. This does *not* trigger an update from the vehicle, it just gets
the data from the BMW servers. This service does not require any attributes.
+21 -4
View File
@@ -11,6 +11,7 @@ from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import (
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
CONF_IGNORE_AVAILABILITY, CONF_SEARCH,
GoogleCalendarService)
from homeassistant.util import Throttle, dt
@@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime',
'maxResults': 1,
'maxResults': 5,
'singleEvents': True,
}
@@ -45,18 +46,22 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar,
data.get('search', None))
data.get(CONF_SEARCH),
data.get(CONF_IGNORE_AVAILABILITY))
super().__init__(hass, data)
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
def __init__(self, calendar_service, calendar_id, search=None):
def __init__(self, calendar_service, calendar_id, search,
ignore_availability):
"""Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self.search = search
self.ignore_availability = ignore_availability
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
@@ -80,5 +85,17 @@ class GoogleCalendarData(object):
result = events.list(**params).execute()
items = result.get('items', [])
self.event = items[0] if len(items) == 1 else None
new_event = None
for item in items:
if (not self.ignore_availability
and 'transparency' in item.keys()):
if item['transparency'] == 'opaque':
new_event = item
break
else:
new_event = item
break
self.event = new_event
return True
+24 -19
View File
@@ -1,21 +1,26 @@
# Describes the format for available calendar services
todoist:
new_task:
description: Create a new task and add it to a project.
fields:
content:
description: The name of the task (Required).
example: Pick up the mail
project:
description: The name of the project this task should belong to. Defaults to Inbox (Optional).
example: Errands
labels:
description: Any labels that you want to apply to this task, separated by a comma (Optional).
example: Chores,Deliveries
priority:
description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional).
example: 2
due_date:
description: The day this task is due, in format YYYY-MM-DD (Optional).
example: "2018-04-01"
todoist_new_task:
description: Create a new task and add it to a project.
fields:
content:
description: The name of the task.
example: Pick up the mail
project:
description: The name of the project this task should belong to. Defaults to Inbox.
example: Errands
labels:
description: Any labels that you want to apply to this task, separated by a comma.
example: Chores,Deliveries
priority:
description: The priority of this task, from 1 (normal) to 4 (urgent).
example: 2
due_date_string:
description: The day this task is due, in natural language.
example: "tomorrow"
due_date_lang:
description: The language of due_date_string.
example: "en"
due_date:
description: The day this task is due, in format YYYY-MM-DD.
example: "2018-04-01"
+19 -1
View File
@@ -41,6 +41,14 @@ CONTENT = 'content'
DESCRIPTION = 'description'
# Calendar Platform: Used in the '_get_date()' method
DATETIME = 'dateTime'
# Service Call: When is this task due (in natural language)?
DUE_DATE_STRING = 'due_date_string'
# Service Call: The language of DUE_DATE_STRING
DUE_DATE_LANG = 'due_date_lang'
# Service Call: The available options of DUE_DATE_LANG
DUE_DATE_VALID_LANGS = ['en', 'da', 'pl', 'zh', 'ko', 'de',
'pt', 'ja', 'it', 'fr', 'sv', 'ru',
'es', 'nl']
# Attribute: When is this task due?
# Service Call: When is this task due?
DUE_DATE = 'due_date'
@@ -83,7 +91,11 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema({
vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
vol.Optional(LABELS): cv.ensure_list_csv,
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
vol.Optional(DUE_DATE): cv.string,
vol.Exclusive(DUE_DATE_STRING, 'due_date'): cv.string,
vol.Optional(DUE_DATE_LANG):
vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)),
vol.Exclusive(DUE_DATE, 'due_date'): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -186,6 +198,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if PRIORITY in call.data:
item.update(priority=call.data[PRIORITY])
if DUE_DATE_STRING in call.data:
item.update(date_string=call.data[DUE_DATE_STRING])
if DUE_DATE_LANG in call.data:
item.update(date_lang=call.data[DUE_DATE_LANG])
if DUE_DATE in call.data:
due_date = dt.parse_datetime(call.data[DUE_DATE])
if due_date is None:
@@ -40,6 +40,7 @@ STATE_HEAT = 'heat'
STATE_COOL = 'cool'
STATE_IDLE = 'idle'
STATE_AUTO = 'auto'
STATE_MANUAL = 'manual'
STATE_DRY = 'dry'
STATE_FAN_ONLY = 'fan_only'
STATE_ECO = 'eco'
+153
View File
@@ -0,0 +1,153 @@
"""
Support for AVM Fritz!Box smarthome thermostate devices.
For more details about this component, please refer to the documentation at
http://home-assistant.io/components/climate.fritzbox/
"""
import logging
import requests
from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN
from homeassistant.components.fritzbox import (
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED)
from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (
ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS)
DEPENDENCIES = ['fritzbox']
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
OPERATION_LIST = [STATE_HEAT, STATE_ECO]
MIN_TEMPERATURE = 8
MAX_TEMPERATURE = 28
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Fritzbox smarthome thermostat platform."""
devices = []
fritz_list = hass.data[FRITZBOX_DOMAIN]
for fritz in fritz_list:
device_list = fritz.get_devices()
for device in device_list:
if device.has_thermostat:
devices.append(FritzboxThermostat(device, fritz))
add_devices(devices)
class FritzboxThermostat(ClimateDevice):
"""The thermostat class for Fritzbox smarthome thermostates."""
def __init__(self, device, fritz):
"""Initialize the thermostat."""
self._device = device
self._fritz = fritz
self._current_temperature = self._device.actual_temperature
self._target_temperature = self._device.target_temperature
self._comfort_temperature = self._device.comfort_temperature
self._eco_temperature = self._device.eco_temperature
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def available(self):
"""Return if thermostat is available."""
return self._device.present
@property
def name(self):
"""Return the name of the device."""
return self._device.name
@property
def temperature_unit(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@property
def precision(self):
"""Return precision 0.5."""
return PRECISION_HALVES
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, **kwargs):
"""Set new target temperature."""
if ATTR_OPERATION_MODE in kwargs:
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
self.set_operation_mode(operation_mode)
elif ATTR_TEMPERATURE in kwargs:
temperature = kwargs.get(ATTR_TEMPERATURE)
self._device.set_target_temperature(temperature)
@property
def current_operation(self):
"""Return the current operation mode."""
if self._target_temperature == self._comfort_temperature:
return STATE_HEAT
elif self._target_temperature == self._eco_temperature:
return STATE_ECO
return STATE_MANUAL
@property
def operation_list(self):
"""Return the list of available operation modes."""
return OPERATION_LIST
def set_operation_mode(self, operation_mode):
"""Set new operation mode."""
if operation_mode == STATE_HEAT:
self.set_temperature(temperature=self._comfort_temperature)
elif operation_mode == STATE_ECO:
self.set_temperature(temperature=self._eco_temperature)
@property
def min_temp(self):
"""Return the minimum temperature."""
return MIN_TEMPERATURE
@property
def max_temp(self):
"""Return the maximum temperature."""
return MAX_TEMPERATURE
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
attrs = {
ATTR_STATE_DEVICE_LOCKED: self._device.device_lock,
ATTR_STATE_LOCKED: self._device.lock,
ATTR_STATE_BATTERY_LOW: self._device.battery_low,
}
return attrs
def update(self):
"""Update the data from the thermostat."""
try:
self._device.update()
self._current_temperature = self._device.actual_temperature
self._target_temperature = self._device.target_temperature
self._comfort_temperature = self._device.comfort_temperature
self._eco_temperature = self._device.eco_temperature
except requests.exceptions.HTTPError as ex:
_LOGGER.warning("Fritzbox connection error: %s", ex)
self._fritz.login()
+13
View File
@@ -38,7 +38,10 @@ class HiveClimateEntity(ClimateDevice):
self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"]
if self.device_type == "Heating":
self.thermostat_node_id = hivedevice["Thermostat_NodeID"]
self.session = hivesession
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
@@ -71,6 +74,11 @@ class HiveClimateEntity(ClimateDevice):
friendly_name = "Hot Water"
return friendly_name
@property
def device_state_attributes(self):
"""Show Device Attributes."""
return self.attributes
@property
def temperature_unit(self):
"""Return the unit of measurement."""
@@ -175,4 +183,9 @@ class HiveClimateEntity(ClimateDevice):
def update(self):
"""Update all Node data from Hive."""
node = self.node_id
if self.device_type == "Heating":
node = self.thermostat_node_id
self.session.core.update_data(self.node_id)
self.attributes = self.session.attributes.state_attributes(node)
@@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE, CONF_REGION)
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.0']
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2']
_LOGGER = logging.getLogger(__name__)
+11 -11
View File
@@ -10,7 +10,7 @@ import logging
from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.components.maxcube import MAXCUBE_HANDLE
from homeassistant.components.maxcube import DATA_KEY
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
_LOGGER = logging.getLogger(__name__)
@@ -24,16 +24,16 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add thermostats."""
cube = hass.data[MAXCUBE_HANDLE].cube
devices = []
for handler in hass.data[DATA_KEY].values():
cube = handler.cube
for device in cube.devices:
name = '{} {}'.format(
cube.room_by_id(device.room_id).name, device.name)
for device in cube.devices:
name = '{} {}'.format(
cube.room_by_id(device.room_id).name, device.name)
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
devices.append(MaxCubeClimate(hass, name, device.rf_address))
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
devices.append(
MaxCubeClimate(handler, name, device.rf_address))
if devices:
add_devices(devices)
@@ -42,14 +42,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MaxCubeClimate(ClimateDevice):
"""MAX! Cube ClimateDevice."""
def __init__(self, hass, name, rf_address):
def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube ClimateDevice."""
self._name = name
self._unit_of_measurement = TEMP_CELSIUS
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
STATE_VACATION]
self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE]
self._cubehandle = handler
@property
def supported_features(self):
+148
View File
@@ -0,0 +1,148 @@
"""
Platform for a Generic Modbus Thermostat.
This uses a setpoint and process
value within the controller, so both the current temperature register and the
target temperature register need to be configured.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.modbus/
"""
import logging
import struct
import voluptuous as vol
from homeassistant.const import (
CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE)
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
import homeassistant.components.modbus as modbus
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['modbus']
# Parameters not defined by homeassistant.const
CONF_TARGET_TEMP = 'target_temp_register'
CONF_CURRENT_TEMP = 'current_temp_register'
CONF_DATA_TYPE = 'data_type'
CONF_COUNT = 'data_count'
CONF_PRECISION = 'precision'
DATA_TYPE_INT = 'int'
DATA_TYPE_UINT = 'uint'
DATA_TYPE_FLOAT = 'float'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_SLAVE): cv.positive_int,
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT):
vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]),
vol.Optional(CONF_COUNT, default=2): cv.positive_int,
vol.Optional(CONF_PRECISION, default=1): cv.positive_int
})
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Modbus Thermostat Platform."""
name = config.get(CONF_NAME)
modbus_slave = config.get(CONF_SLAVE)
target_temp_register = config.get(CONF_TARGET_TEMP)
current_temp_register = config.get(CONF_CURRENT_TEMP)
data_type = config.get(CONF_DATA_TYPE)
count = config.get(CONF_COUNT)
precision = config.get(CONF_PRECISION)
add_devices([ModbusThermostat(name, modbus_slave,
target_temp_register, current_temp_register,
data_type, count, precision)], True)
class ModbusThermostat(ClimateDevice):
"""Representation of a Modbus Thermostat."""
def __init__(self, name, modbus_slave, target_temp_register,
current_temp_register, data_type, count, precision):
"""Initialize the unit."""
self._name = name
self._slave = modbus_slave
self._target_temperature_register = target_temp_register
self._current_temperature_register = current_temp_register
self._target_temperature = None
self._current_temperature = None
self._data_type = data_type
self._count = int(count)
self._precision = precision
self._structure = '>f'
data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'},
DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'},
DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}}
self._structure = '>{}'.format(data_types[self._data_type]
[self._count])
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def update(self):
"""Update Target & Current Temperature."""
self._target_temperature = self.read_register(
self._target_temperature_register)
self._current_temperature = self.read_register(
self._current_temperature_register)
@property
def name(self):
"""Return the name of the climate device."""
return self._name
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temperature = kwargs.get(ATTR_TEMPERATURE)
if target_temperature is None:
return
byte_string = struct.pack(self._structure, target_temperature)
register_value = struct.unpack('>h', byte_string[0:2])[0]
try:
self.write_register(self._target_temperature_register,
register_value)
except AttributeError as ex:
_LOGGER.error(ex)
def read_register(self, register):
"""Read holding register using the modbus hub slave."""
try:
result = modbus.HUB.read_holding_registers(self._slave, register,
self._count)
except AttributeError as ex:
_LOGGER.error(ex)
byte_string = b''.join(
[x.to_bytes(2, byteorder='big') for x in result.registers])
val = struct.unpack(self._structure, byte_string)[0]
register_value = format(val, '.{}f'.format(self._precision))
return register_value
def write_register(self, register, value):
"""Write register using the modbus hub slave."""
modbus.HUB.write_registers(self._slave, register, [value, 0])
+5
View File
@@ -187,6 +187,11 @@ class NestThermostat(ClimateDevice):
device_mode = operation_mode
elif operation_mode == STATE_AUTO:
device_mode = NEST_MODE_HEAT_COOL
else:
device_mode = STATE_OFF
_LOGGER.error(
"An error occurred while setting device mode. "
"Invalid operation mode: %s", operation_mode)
self.device.mode = device_mode
@property
+22 -27
View File
@@ -18,37 +18,26 @@ SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
ON_DEMAND = ('zwave',)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the config component."""
yield from hass.components.frontend.async_register_built_in_panel(
await hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'mdi:settings')
@asyncio.coroutine
def setup_panel(panel_name):
async def setup_panel(panel_name):
"""Set up a panel."""
panel = yield from async_prepare_setup_platform(
panel = await async_prepare_setup_platform(
hass, config, DOMAIN, panel_name)
if not panel:
return
success = yield from panel.async_setup(hass)
success = await panel.async_setup(hass)
if success:
key = '{}.{}'.format(DOMAIN, panel_name)
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
hass.config.components.add(key)
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
for panel_name in ON_DEMAND:
if panel_name in hass.config.components:
tasks.append(setup_panel(panel_name))
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
@callback
def component_loaded(event):
"""Respond to components being loaded."""
@@ -58,6 +47,15 @@ def async_setup(hass, config):
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
for panel_name in ON_DEMAND:
if panel_name in hass.config.components:
tasks.append(setup_panel(panel_name))
if tasks:
await asyncio.wait(tasks, loop=hass.loop)
return True
@@ -86,11 +84,10 @@ class BaseEditConfigView(HomeAssistantView):
"""Set value."""
raise NotImplementedError
@asyncio.coroutine
def get(self, request, config_key):
async def get(self, request, config_key):
"""Fetch device specific config."""
hass = request.app['hass']
current = yield from self.read_config(hass)
current = await self.read_config(hass)
value = self._get_value(hass, current, config_key)
if value is None:
@@ -98,11 +95,10 @@ class BaseEditConfigView(HomeAssistantView):
return self.json(value)
@asyncio.coroutine
def post(self, request, config_key):
async def post(self, request, config_key):
"""Validate config and return results."""
try:
data = yield from request.json()
data = await request.json()
except ValueError:
return self.json_message('Invalid JSON specified', 400)
@@ -121,10 +117,10 @@ class BaseEditConfigView(HomeAssistantView):
hass = request.app['hass']
path = hass.config.path(self.path)
current = yield from self.read_config(hass)
current = await self.read_config(hass)
self._write_value(hass, current, config_key, data)
yield from hass.async_add_job(_write, path, current)
await hass.async_add_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_add_job(self.post_write_hook(hass))
@@ -133,10 +129,9 @@ class BaseEditConfigView(HomeAssistantView):
'result': 'ok',
})
@asyncio.coroutine
def read_config(self, hass):
async def read_config(self, hass):
"""Read the config."""
current = yield from hass.async_add_job(
current = await hass.async_add_job(
_read, hass.config.path(self.path))
if not current:
current = self._empty_config()
+39 -1
View File
@@ -1,6 +1,9 @@
"""Provide configuration end points for Automations."""
import asyncio
from collections import OrderedDict
import uuid
from homeassistant.const import CONF_ID
from homeassistant.components.config import EditIdBasedConfigView
from homeassistant.components.automation import (
PLATFORM_SCHEMA, DOMAIN, async_reload)
@@ -13,8 +16,43 @@ CONFIG_PATH = 'automations.yaml'
@asyncio.coroutine
def async_setup(hass):
"""Set up the Automation config API."""
hass.http.register_view(EditIdBasedConfigView(
hass.http.register_view(EditAutomationConfigView(
DOMAIN, 'config', CONFIG_PATH, cv.string,
PLATFORM_SCHEMA, post_write_hook=async_reload
))
return True
class EditAutomationConfigView(EditIdBasedConfigView):
"""Edit automation config."""
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
index = None
for index, cur_value in enumerate(data):
# When people copy paste their automations to the config file,
# they sometimes forget to add IDs. Fix it here.
if CONF_ID not in cur_value:
cur_value[CONF_ID] = uuid.uuid4().hex
elif cur_value[CONF_ID] == config_key:
break
else:
cur_value = OrderedDict()
cur_value[CONF_ID] = config_key
index = len(data)
data.append(cur_value)
# Iterate through some keys that we want to have ordered in the output
updated_value = OrderedDict()
for key in ('id', 'alias', 'trigger', 'condition', 'action'):
if key in cur_value:
updated_value[key] = cur_value[key]
if key in new_value:
updated_value[key] = new_value[key]
# We cover all current fields above, but just in case we start
# supporting more fields in the future.
updated_value.update(cur_value)
updated_value.update(new_value)
data[index] = updated_value
@@ -1,11 +1,10 @@
"""Http views to control the config manager."""
import asyncio
import voluptuous as vol
from homeassistant import config_entries
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
REQUIREMENTS = ['voluptuous-serialize==1']
@@ -16,15 +15,17 @@ def async_setup(hass):
"""Enable the Home Assistant views."""
hass.http.register_view(ConfigManagerEntryIndexView)
hass.http.register_view(ConfigManagerEntryResourceView)
hass.http.register_view(ConfigManagerFlowIndexView)
hass.http.register_view(ConfigManagerFlowResourceView)
hass.http.register_view(
ConfigManagerFlowIndexView(hass.config_entries.flow))
hass.http.register_view(
ConfigManagerFlowResourceView(hass.config_entries.flow))
hass.http.register_view(ConfigManagerAvailableFlowView)
return True
def _prepare_json(result):
"""Convert result for JSON."""
if result['type'] != config_entries.RESULT_TYPE_FORM:
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
@@ -78,7 +79,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
return self.json(result)
class ConfigManagerFlowIndexView(HomeAssistantView):
class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""View to create config flows."""
url = '/api/config/config_entries/flow'
@@ -94,81 +95,16 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
hass = request.app['hass']
return self.json([
flow for flow in hass.config_entries.flow.async_progress()
if flow['source'] != config_entries.SOURCE_USER])
@RequestDataValidator(vol.Schema({
vol.Required('domain'): str,
}))
@asyncio.coroutine
def post(self, request, data):
"""Handle a POST request."""
hass = request.app['hass']
try:
result = yield from hass.config_entries.flow.async_init(
data['domain'])
except config_entries.UnknownHandler:
return self.json_message('Invalid handler specified', 404)
except config_entries.UnknownStep:
return self.json_message('Handler does not support init', 400)
result = _prepare_json(result)
return self.json(result)
flw for flw in hass.config_entries.flow.async_progress()
if flw['source'] != data_entry_flow.SOURCE_USER])
class ConfigManagerFlowResourceView(HomeAssistantView):
class ConfigManagerFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager."""
url = '/api/config/config_entries/flow/{flow_id}'
name = 'api:config:config_entries:flow:resource'
@asyncio.coroutine
def get(self, request, flow_id):
"""Get the current state of a flow."""
hass = request.app['hass']
try:
result = yield from hass.config_entries.flow.async_configure(
flow_id)
except config_entries.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
result = _prepare_json(result)
return self.json(result)
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
@asyncio.coroutine
def post(self, request, flow_id, data):
"""Handle a POST request."""
hass = request.app['hass']
try:
result = yield from hass.config_entries.flow.async_configure(
flow_id, data)
except config_entries.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
except vol.Invalid:
return self.json_message('User input malformed', 400)
result = _prepare_json(result)
return self.json(result)
@asyncio.coroutine
def delete(self, request, flow_id):
"""Cancel a flow in progress."""
hass = request.app['hass']
try:
hass.config_entries.flow.async_abort(flow_id)
except config_entries.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
return self.json_message('Flow aborted')
class ConfigManagerAvailableFlowView(HomeAssistantView):
"""View to query available flows."""
+6 -10
View File
@@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.components.cover import (
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN,
CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED,
CONF_IP_ADDRESS, CONF_NAME)
import homeassistant.helpers.config_validation as cv
@@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(MyGogogate2Device(
mygogogate2, door, name) for door in devices)
return
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
@@ -60,7 +59,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return
class MyGogogate2Device(CoverDevice):
@@ -72,7 +70,7 @@ class MyGogogate2Device(CoverDevice):
self.device_id = device['door']
self._name = name or device['name']
self._status = device['status']
self.available = None
self._available = None
@property
def name(self):
@@ -97,24 +95,22 @@ class MyGogogate2Device(CoverDevice):
@property
def available(self):
"""Could the device be accessed during the last update call."""
return self.available
return self._available
def close_cover(self, **kwargs):
"""Issue close command to cover."""
self.mygogogate2.close_device(self.device_id)
self.schedule_update_ha_state(True)
def open_cover(self, **kwargs):
"""Issue open command to cover."""
self.mygogogate2.open_device(self.device_id)
self.schedule_update_ha_state(True)
def update(self):
"""Update status of cover."""
try:
self._status = self.mygogogate2.get_status(self.device_id)
self.available = True
self._available = True
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
self._status = STATE_UNKNOWN
self.available = False
self._status = None
self._available = False
+24 -23
View File
@@ -18,30 +18,31 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ATTR_DISTANCE_SENSOR = "distance_sensor"
ATTR_DOOR_STATE = "door_state"
ATTR_SIGNAL_STRENGTH = "wifi_signal"
ATTR_DISTANCE_SENSOR = 'distance_sensor'
ATTR_DOOR_STATE = 'door_state'
ATTR_SIGNAL_STRENGTH = 'wifi_signal'
CONF_DEVICEKEY = "device_key"
CONF_DEVICE_ID = 'device_id'
CONF_DEVICE_KEY = 'device_key'
DEFAULT_NAME = 'OpenGarage'
DEFAULT_PORT = 80
STATE_CLOSING = "closing"
STATE_OFFLINE = "offline"
STATE_OPENING = "opening"
STATE_STOPPED = "stopped"
STATE_CLOSING = 'closing'
STATE_OFFLINE = 'offline'
STATE_OPENING = 'opening'
STATE_STOPPED = 'stopped'
STATES_MAP = {
0: STATE_CLOSED,
1: STATE_OPEN
1: STATE_OPEN,
}
COVER_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICEKEY): cv.string,
vol.Required(CONF_DEVICE_KEY): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME): cv.string
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -50,7 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up OpenGarage covers."""
"""Set up the OpenGarage covers."""
covers = []
devices = config.get(CONF_COVERS)
@@ -59,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
CONF_NAME: device_config.get(CONF_NAME),
CONF_HOST: device_config.get(CONF_HOST),
CONF_PORT: device_config.get(CONF_PORT),
"device_id": device_config.get(CONF_DEVICE, device_id),
CONF_DEVICEKEY: device_config.get(CONF_DEVICEKEY)
CONF_DEVICE_ID: device_config.get(CONF_DEVICE, device_id),
CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY)
}
covers.append(OpenGarageCover(hass, args))
@@ -79,8 +80,8 @@ class OpenGarageCover(CoverDevice):
self.hass = hass
self._name = args[CONF_NAME]
self.device_id = args['device_id']
self._devicekey = args[CONF_DEVICEKEY]
self._state = STATE_UNKNOWN
self._device_key = args[CONF_DEVICE_KEY]
self._state = None
self._state_before_move = None
self.dist = None
self.signal = None
@@ -138,8 +139,8 @@ class OpenGarageCover(CoverDevice):
try:
status = self._get_status()
if self._name is None:
if status["name"] is not None:
self._name = status["name"]
if status['name'] is not None:
self._name = status['name']
state = STATES_MAP.get(status.get('door'), STATE_UNKNOWN)
if self._state_before_move is not None:
if self._state_before_move != state:
@@ -152,7 +153,7 @@ class OpenGarageCover(CoverDevice):
self.signal = status.get('rssi')
self.dist = status.get('dist')
self._available = True
except (requests.exceptions.RequestException) as ex:
except requests.exceptions.RequestException as ex:
_LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
dict(reason=ex))
self._state = STATE_OFFLINE
@@ -166,15 +167,15 @@ class OpenGarageCover(CoverDevice):
def _push_button(self):
"""Send commands to API."""
url = '{}/cc?dkey={}&click=1'.format(
self.opengarage_url, self._devicekey)
self.opengarage_url, self._device_key)
try:
response = requests.get(url, timeout=10).json()
if response["result"] == 2:
_LOGGER.error("Unable to control %s: device_key is incorrect.",
if response['result'] == 2:
_LOGGER.error("Unable to control %s: Device key is incorrect",
self._name)
self._state = self._state_before_move
self._state_before_move = None
except (requests.exceptions.RequestException) as ex:
except requests.exceptions.RequestException as ex:
_LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
dict(reason=ex))
self._state = self._state_before_move
+1 -1
View File
@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tahoma covers."""
"""Set up the Tahoma covers."""
controller = hass.data[TAHOMA_DOMAIN]['controller']
devices = []
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:
@@ -18,6 +18,7 @@
"no_key": "Couldn't get an API key"
},
"abort": {
"already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
+36 -170
View File
@@ -4,28 +4,20 @@ Support for deCONZ devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/
"""
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery, aiohttp_client
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json
from homeassistant.helpers import (
aiohttp_client, discovery, config_validation as cv)
from homeassistant.util.json import load_json
REQUIREMENTS = ['pydeconz==35']
# Loading the config flow file will register the flow
from .config_flow import configured_hosts
from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'deconz'
DATA_DECONZ_ID = 'deconz_entities'
CONFIG_FILE = 'deconz.conf'
REQUIREMENTS = ['pydeconz==36']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -46,46 +38,38 @@ SERVICE_SCHEMA = vol.Schema({
})
CONFIG_INSTRUCTIONS = """
Unlock your deCONZ gateway to register with Home Assistant.
1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
2. Press "Unlock Gateway" button
[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
"""
async def async_setup(hass, config):
"""Set up services and configuration for deCONZ component."""
result = False
config_file = await hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE))
async def async_deconz_discovered(service, discovery_info):
"""Call when deCONZ gateway has been found."""
deconz_config = {}
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
await async_request_configuration(hass, config, deconz_config)
if config_file:
result = await async_setup_deconz(hass, config, config_file)
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN]
if CONF_API_KEY in deconz_config:
result = await async_setup_deconz(hass, config, deconz_config)
else:
await async_request_configuration(hass, config, deconz_config)
return True
if not result:
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
"""Load configuration for deCONZ component.
Discovery has loaded the component if DOMAIN is not present in config.
"""
if DOMAIN in config:
deconz_config = None
config_file = await hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE))
if config_file:
deconz_config = config_file
elif CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN]
if deconz_config and not configured_hosts(hass):
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data=deconz_config
))
return True
async def async_setup_entry(hass, entry):
"""Set up a deCONZ bridge for a config entry."""
if DOMAIN in hass.data:
_LOGGER.error(
"Config entry failed since one deCONZ instance already exists")
return False
result = await async_setup_deconz(hass, None, entry.data)
if result:
return True
return False
async def async_setup_deconz(hass, config, deconz_config):
"""Set up a deCONZ session.
@@ -94,8 +78,8 @@ async def async_setup_deconz(hass, config, deconz_config):
"""
_LOGGER.debug("deCONZ config %s", deconz_config)
from pydeconz import DeconzSession
websession = async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, websession, **deconz_config)
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **deconz_config)
result = await deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ")
@@ -152,121 +136,3 @@ async def async_setup_deconz(hass, config, deconz_config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
return True
async def async_request_configuration(hass, config, deconz_config):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
async def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key
websession = async_get_clientsession(hass)
api_key = await async_get_api_key(websession, **deconz_config)
if api_key:
deconz_config[CONF_API_KEY] = api_key
result = await async_setup_deconz(hass, config, deconz_config)
if result:
await hass.async_add_job(
save_json, hass.config.path(CONFIG_FILE), deconz_config)
configurator.async_request_done(request_id)
return
else:
configurator.async_notify_errors(
request_id, "Couldn't load configuration.")
else:
configurator.async_notify_errors(
request_id, "Couldn't get an API key.")
return
instructions = CONFIG_INSTRUCTIONS.format(
deconz_config[CONF_HOST], deconz_config[CONF_PORT])
request_id = configurator.async_request_config(
"deCONZ", async_configuration_callback,
description=instructions,
entity_picture="/static/images/logo_deconz.jpeg",
submit_caption="I have unlocked the gateway",
)
@config_entries.HANDLERS.register(DOMAIN)
class DeconzFlowHandler(config_entries.ConfigFlowHandler):
"""Handle a deCONZ config flow."""
VERSION = 1
def __init__(self):
"""Initialize the deCONZ flow."""
self.bridges = []
self.deconz_config = {}
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from pydeconz.utils import async_discovery
if DOMAIN in self.hass.data:
return self.async_abort(
reason='one_instance_only'
)
if user_input is not None:
for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]:
self.deconz_config = bridge
return await self.async_step_link()
session = aiohttp_client.async_get_clientsession(self.hass)
self.bridges = await async_discovery(session)
if len(self.bridges) == 1:
self.deconz_config = self.bridges[0]
return await self.async_step_link()
elif len(self.bridges) > 1:
hosts = []
for bridge in self.bridges:
hosts.append(bridge[CONF_HOST])
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required(CONF_HOST): vol.In(hosts)
})
)
return self.async_abort(
reason='no_bridges'
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
from pydeconz.utils import async_get_api_key
errors = {}
if user_input is not None:
session = aiohttp_client.async_get_clientsession(self.hass)
api_key = await async_get_api_key(session, **self.deconz_config)
if api_key:
self.deconz_config[CONF_API_KEY] = api_key
return self.async_create_entry(
title='deCONZ',
data=self.deconz_config
)
else:
errors['base'] = 'no_key'
return self.async_show_form(
step_id='link',
errors=errors,
)
async def async_setup_entry(hass, entry):
"""Set up a bridge for a config entry."""
if DOMAIN in hass.data:
_LOGGER.error(
"Config entry failed since one deCONZ instance already exists")
return False
result = await async_setup_deconz(hass, None, entry.data)
if result:
return True
return False
@@ -0,0 +1,139 @@
"""Config flow to configure deCONZ component."""
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant.core import callback
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.helpers import aiohttp_client
from homeassistant.util.json import load_json
from .const import CONFIG_FILE, DOMAIN
@callback
def configured_hosts(hass):
"""Return a set of the configured hosts."""
return set(entry.data['host'] for entry
in hass.config_entries.async_entries(DOMAIN))
@config_entries.HANDLERS.register(DOMAIN)
class DeconzFlowHandler(data_entry_flow.FlowHandler):
"""Handle a deCONZ config flow."""
VERSION = 1
def __init__(self):
"""Initialize the deCONZ config flow."""
self.bridges = []
self.deconz_config = {}
async def async_step_init(self, user_input=None):
"""Handle a deCONZ config flow start."""
from pydeconz.utils import async_discovery
if configured_hosts(self.hass):
return self.async_abort(reason='one_instance_only')
if user_input is not None:
for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]:
self.deconz_config = bridge
return await self.async_step_link()
session = aiohttp_client.async_get_clientsession(self.hass)
self.bridges = await async_discovery(session)
if len(self.bridges) == 1:
self.deconz_config = self.bridges[0]
return await self.async_step_link()
elif len(self.bridges) > 1:
hosts = []
for bridge in self.bridges:
hosts.append(bridge[CONF_HOST])
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required(CONF_HOST): vol.In(hosts)
})
)
return self.async_abort(
reason='no_bridges'
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
from pydeconz.utils import async_get_api_key, async_get_bridgeid
errors = {}
if user_input is not None:
if configured_hosts(self.hass):
return self.async_abort(reason='one_instance_only')
session = aiohttp_client.async_get_clientsession(self.hass)
api_key = await async_get_api_key(session, **self.deconz_config)
if api_key:
self.deconz_config[CONF_API_KEY] = api_key
if 'bridgeid' not in self.deconz_config:
self.deconz_config['bridgeid'] = await async_get_bridgeid(
session, **self.deconz_config)
return self.async_create_entry(
title='deCONZ-' + self.deconz_config['bridgeid'],
data=self.deconz_config
)
errors['base'] = 'no_key'
return self.async_show_form(
step_id='link',
errors=errors,
)
async def async_step_discovery(self, discovery_info):
"""Prepare configuration for a discovered deCONZ bridge.
This flow is triggered by the discovery component.
"""
deconz_config = {}
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
deconz_config['bridgeid'] = discovery_info.get('serial')
config_file = await self.hass.async_add_job(
load_json, self.hass.config.path(CONFIG_FILE))
if config_file and \
config_file[CONF_HOST] == deconz_config[CONF_HOST] and \
CONF_API_KEY in config_file:
deconz_config[CONF_API_KEY] = config_file[CONF_API_KEY]
return await self.async_step_import(deconz_config)
async def async_step_import(self, import_config):
"""Import a deCONZ bridge as a config entry.
This flow is triggered by `async_setup` for configured bridges.
This flow is also triggered by `async_step_discovery`.
This will execute for any bridge that does not have a
config entry yet (based on host).
If an API key is provided, we will create an entry.
Otherwise we will delegate to `link` step which
will ask user to link the bridge.
"""
from pydeconz.utils import async_get_bridgeid
if configured_hosts(self.hass):
return self.async_abort(reason='one_instance_only')
elif CONF_API_KEY not in import_config:
self.deconz_config = import_config
return await self.async_step_link()
if 'bridgeid' not in import_config:
session = aiohttp_client.async_get_clientsession(self.hass)
import_config['bridgeid'] = await async_get_bridgeid(
session, **import_config)
return self.async_create_entry(
title='deCONZ-' + import_config['bridgeid'],
data=import_config
)
+8
View File
@@ -0,0 +1,8 @@
"""Constants for the deCONZ component."""
import logging
_LOGGER = logging.getLogger('homeassistant.components.deconz')
DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf'
DATA_DECONZ_ID = 'deconz_entities'
@@ -18,6 +18,7 @@
"no_key": "Couldn't get an API key"
},
"abort": {
"already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
@@ -9,8 +9,6 @@ from datetime import timedelta
import logging
from typing import Any, List, Sequence, Callable
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform
@@ -19,7 +17,6 @@ from homeassistant.loader import bind_hass
from homeassistant.components import group, zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
@@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac'
ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type'
ATTR_VENDOR = 'vendor'
ATTR_CONSIDER_HOME = 'consider_home'
SOURCE_TYPE_GPS = 'gps'
@@ -328,14 +324,10 @@ class DeviceTracker(object):
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
# lookup mac vendor string to be stored in config
yield from device.set_vendor_for_mac()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac,
ATTR_VENDOR: device.vendor,
})
# update known_devices.yaml
@@ -413,7 +405,6 @@ class Device(Entity):
consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int
attributes = None # type: dict
vendor = None # type: str
icon = None # type: str
# Track if the last update of this device was HOME.
@@ -423,7 +414,7 @@ class Device(Entity):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str = None,
picture: str = None, gravatar: str = None, icon: str = None,
hide_if_away: bool = False, vendor: str = None) -> None:
hide_if_away: bool = False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@@ -451,7 +442,6 @@ class Device(Entity):
self.icon = icon
self.away_hide = hide_if_away
self.vendor = vendor
self.source_type = None
@@ -567,51 +557,6 @@ class Device(Entity):
self._state = STATE_HOME
self.last_update_home = True
@asyncio.coroutine
def set_vendor_for_mac(self):
"""Set vendor string using api.macvendors.com."""
self.vendor = yield from self.get_vendor_for_mac()
@asyncio.coroutine
def get_vendor_for_mac(self):
"""Try to find the vendor string for a given MAC address."""
if not self.mac:
return None
if '_' in self.mac:
_, mac = self.mac.split('_', 1)
else:
mac = self.mac
if not len(mac.split(':')) == 6:
return 'unknown'
# We only need the first 3 bytes of the MAC for a lookup
# this improves somewhat on privacy
oui_bytes = mac.split(':')[0:3]
# bytes like 00 get truncates to 0, API needs full bytes
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
url = 'http://api.macvendors.com/' + oui
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(5, loop=self.hass.loop):
resp = yield from websession.get(url)
# mac vendor found, response is the string
if resp.status == 200:
vendor_string = yield from resp.text()
return vendor_string
# If vendor is not known to the API (404) or there
# was a failure during the lookup (500); set vendor
# to something other then None to prevent retry
# as the value is only relevant when it is to be stored
# in the 'known_devices.yaml' file which only happens
# the first time the device is seen.
return 'unknown'
except (asyncio.TimeoutError, aiohttp.ClientError):
# Same as above
return 'unknown'
@asyncio.coroutine
def async_added_to_hass(self):
"""Add an entity."""
@@ -660,6 +605,17 @@ class DeviceScanner(object):
"""
return self.hass.async_add_job(self.get_device_name, device)
def get_extra_attributes(self, device: str) -> dict:
"""Get the extra attributes of a device."""
raise NotImplementedError()
def async_get_extra_attributes(self, device: str) -> Any:
"""Get the extra attributes of a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.get_extra_attributes, device)
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file."""
@@ -685,7 +641,6 @@ def async_load_config(path: str, hass: HomeAssistantType,
vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional('vendor', default=None): vol.Any(None, cv.string),
})
try:
result = []
@@ -697,6 +652,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
return []
for dev_id, device in devices.items():
# Deprecated option. We just ignore it to avoid breaking change
device.pop('vendor', None)
try:
device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id)
@@ -744,10 +701,20 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
host_name = yield from scanner.async_get_device_name(mac)
seen.add(mac)
try:
extra_attributes = (yield from
scanner.async_get_extra_attributes(mac))
except NotImplementedError:
extra_attributes = dict()
kwargs = {
'mac': mac,
'host_name': host_name,
'source_type': SOURCE_TYPE_ROUTER
'source_type': SOURCE_TYPE_ROUTER,
'attributes': {
'scanner': scanner.__class__.__name__,
**extra_attributes
}
}
zone_home = hass.states.get(zone.ENTITY_ID_HOME)
@@ -772,7 +739,6 @@ def update_config(path: str, dev_id: str, device: Device):
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,
'vendor': device.vendor,
}}
out.write('\n')
out.write(dump(device))
@@ -40,7 +40,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
attributes = {}
if rssi is not None:
attributes['rssi'] = rssi
see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name,
see(mac="{}{}".format(BT_PREFIX, mac), host_name=name,
attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
def discover_devices():
@@ -48,8 +48,11 @@ class BMWDeviceTracker(object):
return
_LOGGER.debug('Updating %s', dev_id)
attrs = {
'vin': self.vehicle.vin,
}
self._see(
dev_id=dev_id, host_name=self.vehicle.name,
gps=self.vehicle.state.gps_position, icon='mdi:car'
gps=self.vehicle.state.gps_position, attributes=attrs,
icon='mdi:car'
)
@@ -19,7 +19,7 @@ from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['locationsharinglib==0.4.0']
REQUIREMENTS = ['locationsharinglib==1.2.1']
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
@@ -176,7 +176,7 @@ class MikrotikScanner(DeviceScanner):
for device in device_names
if device.get('mac-address')}
if self.wireless_exist:
if self.wireless_exist or self.capsman_exist:
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
@@ -80,6 +80,8 @@ class NmapDeviceScanner(DeviceScanner):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
_LOGGER.debug("Nmap last results %s", self.last_results)
return [device.mac for device in self.last_results]
def get_device_name(self, device):
@@ -91,6 +93,13 @@ class NmapDeviceScanner(DeviceScanner):
return filter_named[0]
return None
def get_extra_attributes(self, device):
"""Return the IP of the given device."""
filter_ip = next((
result.ip for result in self.last_results
if result.mac == device), None)
return {'ip': filter_ip}
def _update_info(self):
"""Scan the network for devices.
@@ -103,6 +103,9 @@ class UbusDeviceScanner(DeviceScanner):
"""Return the name of the given device or None if we don't know."""
if self.mac2name is None:
self._generate_mac2name()
if self.mac2name is None:
# Generation of mac2name dictionary failed
return None
name = self.mac2name.get(device.upper(), None)
return name
@@ -122,3 +122,9 @@ class UnifiScanner(DeviceScanner):
name = client.get('name') or client.get('hostname')
_LOGGER.debug("Device mac %s name %s", device, name)
return name
def get_extra_attributes(self, device):
"""Return the extra attributes of the device."""
client = self._clients.get(device, {})
_LOGGER.debug("Device mac %s attributes %s", device, client)
return client
@@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
})
REQUIREMENTS = ['python-miio==0.3.9']
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
def get_scanner(hass, config):
@@ -41,7 +41,7 @@ def get_scanner(hass, config):
device_info.model,
device_info.firmware_version,
device_info.hardware_version)
scanner = XiaomiMiioDeviceScanner(hass, device)
scanner = XiaomiMiioDeviceScanner(device)
except DeviceException as ex:
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
@@ -51,7 +51,7 @@ def get_scanner(hass, config):
class XiaomiMiioDeviceScanner(DeviceScanner):
"""This class queries a Xiaomi Mi WiFi Repeater."""
def __init__(self, hass, device):
def __init__(self, device):
"""Initialize the scanner."""
self.device = device
+21 -5
View File
@@ -13,7 +13,7 @@ import os
import voluptuous as vol
from homeassistant import config_entries
from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv
@@ -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==1.3.0']
REQUIREMENTS = ['netdisco==1.3.1']
DOMAIN = 'discovery'
@@ -40,8 +40,10 @@ SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HOMEKIT = 'homekit'
CONFIG_ENTRY_HANDLERS = {
SERVICE_DECONZ: 'deconz',
SERVICE_HUE: 'hue',
}
@@ -56,7 +58,6 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
'google_cast': ('media_player', 'cast'),
@@ -77,14 +78,23 @@ SERVICE_HANDLERS = {
'bose_soundtouch': ('media_player', 'soundtouch'),
'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'),
'kodi': ('media_player', 'kodi'),
}
OPTIONAL_SERVICE_HANDLERS = {
SERVICE_HOMEKIT: ('homekit_controller', None),
}
CONF_IGNORE = 'ignore'
CONF_ENABLE = 'enable'
CONFIG_SCHEMA = vol.Schema({
vol.Required(DOMAIN): vol.Schema({
vol.Optional(CONF_IGNORE, default=[]):
vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)])
vol.All(cv.ensure_list, [
vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]),
vol.Optional(CONF_ENABLE, default=[]):
vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)])
}),
}, extra=vol.ALLOW_EXTRA)
@@ -103,6 +113,9 @@ async def async_setup(hass, config):
# Platforms ignore by config
ignored_platforms = config[DOMAIN][CONF_IGNORE]
# Optional platforms enabled by config
enabled_platforms = config[DOMAIN][CONF_ENABLE]
async def new_service_found(service, info):
"""Handle a new service if one is found."""
if service in ignored_platforms:
@@ -118,13 +131,16 @@ async def async_setup(hass, config):
if service in CONFIG_ENTRY_HANDLERS:
await hass.config_entries.flow.async_init(
CONFIG_ENTRY_HANDLERS[service],
source=config_entries.SOURCE_DISCOVERY,
source=data_entry_flow.SOURCE_DISCOVERY,
data=info
)
return
comp_plat = SERVICE_HANDLERS.get(service)
if not comp_plat and service in enabled_platforms:
comp_plat = OPTIONAL_SERVICE_HANDLERS[service]
# We do not know how to handle this service.
if not comp_plat:
logger.info("Unknown service discovered: %s %s", service, info)
+1 -1
View File
@@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle
from homeassistant.util.json import save_json
REQUIREMENTS = ['python-ecobee-api==0.0.17']
REQUIREMENTS = ['python-ecobee-api==0.0.18']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
+77
View File
@@ -0,0 +1,77 @@
"""
Support for Eufy devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/eufy/
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, \
CONF_DEVICES, CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, CONF_NAME
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['lakeside==0.5']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'eufy'
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_ADDRESS): cv.string,
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Required(CONF_TYPE): cv.string,
vol.Optional(CONF_NAME): cv.string
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list,
[DEVICE_SCHEMA]),
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
EUFY_DISPATCH = {
'T1011': 'light',
'T1012': 'light',
'T1013': 'light',
'T1201': 'switch',
'T1202': 'switch',
'T1211': 'switch'
}
def setup(hass, config):
"""Set up Eufy devices."""
# pylint: disable=import-error
import lakeside
if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]:
data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME],
config[DOMAIN][CONF_PASSWORD])
for device in data:
kind = device['type']
if kind not in EUFY_DISPATCH:
continue
discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device,
config)
for device_info in config[DOMAIN][CONF_DEVICES]:
kind = device_info['type']
if kind not in EUFY_DISPATCH:
continue
device = {}
device['address'] = device_info['address']
device['code'] = device_info['access_token']
device['type'] = device_info['type']
device['name'] = device_info['name']
discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device,
config)
return True
+2 -1
View File
@@ -708,7 +708,7 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
def __init__(self, name, device, model, unique_id):
"""Initialize the plug switch."""
from miio.airpurifier import OperationMode
from miio.airhumidifier import OperationMode
super().__init__(name, device, model, unique_id)
@@ -748,6 +748,7 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return self._speed_list
+83
View File
@@ -0,0 +1,83 @@
"""
Support for AVM Fritz!Box smarthome devices.
For more details about this component, please refer to the documentation at
http://home-assistant.io/components/fritzbox/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyfritzhome==0.3.7']
SUPPORTED_DOMAINS = ['climate', 'switch']
DOMAIN = 'fritzbox'
ATTR_STATE_DEVICE_LOCKED = 'device_locked'
ATTR_STATE_LOCKED = 'locked'
ATTR_STATE_BATTERY_LOW = 'battery_low'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICES):
vol.All(cv.ensure_list, [
vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
}),
]),
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the fritzbox component."""
from pyfritzhome import Fritzhome, LoginError
fritz_list = []
configured_devices = config[DOMAIN].get(CONF_DEVICES)
for device in configured_devices:
host = device.get(CONF_HOST)
username = device.get(CONF_USERNAME)
password = device.get(CONF_PASSWORD)
fritzbox = Fritzhome(host=host, user=username,
password=password)
try:
fritzbox.login()
_LOGGER.info("Connected to device %s", device)
except LoginError:
_LOGGER.warning("Login to Fritz!Box %s as %s failed",
host, username)
continue
fritz_list.append(fritzbox)
if not fritz_list:
_LOGGER.info("No fritzboxes configured")
return False
hass.data[DOMAIN] = fritz_list
def logout_fritzboxes(event):
"""Close all connections to the fritzboxes."""
for fritz in fritz_list:
fritz.logout()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes)
for domain in SUPPORTED_DOMAINS:
discovery.load_platform(hass, domain, DOMAIN, {}, config)
return True
@@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180404.0']
REQUIREMENTS = ['home-assistant-frontend==20180426.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
+3 -1
View File
@@ -44,6 +44,7 @@ CONF_ENTITIES = 'entities'
CONF_TRACK = 'track'
CONF_SEARCH = 'search'
CONF_OFFSET = 'offset'
CONF_IGNORE_AVAILABILITY = 'ignore_availability'
DEFAULT_CONF_TRACK_NEW = True
DEFAULT_CONF_OFFSET = '!!'
@@ -74,8 +75,9 @@ _SINGLE_CALSEARCH_CONFIG = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_TRACK): cv.boolean,
vol.Optional(CONF_SEARCH): vol.Any(cv.string, None),
vol.Optional(CONF_SEARCH): cv.string,
vol.Optional(CONF_OFFSET): cv.string,
vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean,
})
DEVICE_SCHEMA = vol.Schema({
@@ -102,18 +102,23 @@ class _GoogleEntity:
if state.state == STATE_UNAVAILABLE:
return None
entity_config = self.config.entity_config.get(state.entity_id, {})
name = (entity_config.get(CONF_NAME) or state.name).strip()
# If an empty string
if not name:
return None
traits = self.traits()
# Found no supported traits for this entity
if not traits:
return None
entity_config = self.config.entity_config.get(state.entity_id, {})
device = {
'id': state.entity_id,
'name': {
'name': entity_config.get(CONF_NAME) or state.name
'name': name
},
'attributes': {},
'traits': [trait.name for trait in traits],
+1 -1
View File
@@ -35,7 +35,7 @@ CONF_TYPES = 'types'
ICON_UNKNOWN = 'mdi:help'
ICON_AUDIO = 'mdi:speaker'
ICON_PLAYER = 'mdi:play'
ICON_TUNER = 'mdi:nest-thermostat'
ICON_TUNER = 'mdi:radio'
ICON_RECORDER = 'mdi:microphone'
ICON_TV = 'mdi:television'
ICONS_BY_TYPE = {
+24
View File
@@ -118,6 +118,30 @@ def state_changes_during_period(hass, start_time, end_time=None,
return states_to_json(hass, states, start_time, entity_ids)
def get_last_state_changes(hass, number_of_states, entity_id):
"""Return the last number_of_states."""
from homeassistant.components.recorder.models import States
start_time = dt_util.utcnow()
with session_scope(hass=hass) as session:
query = session.query(States).filter(
(States.last_changed == States.last_updated))
if entity_id is not None:
query = query.filter_by(entity_id=entity_id.lower())
entity_ids = [entity_id] if entity_id is not None else None
states = execute(
query.order_by(States.last_updated.desc()).limit(number_of_states))
return states_to_json(hass, reversed(states),
start_time,
entity_ids,
include_start_time_state=False)
def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
filters=None):
"""Return the states at a specific point in time."""
+5 -1
View File
@@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL,
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['pyhiveapi==0.2.11']
REQUIREMENTS = ['pyhiveapi==0.2.14']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'hive'
@@ -44,6 +44,8 @@ class HiveSession:
light = None
sensor = None
switch = None
weather = None
attributes = None
def setup(hass, config):
@@ -70,6 +72,8 @@ def setup(hass, config):
session.hotwater = Pyhiveapi.Hotwater()
session.light = Pyhiveapi.Light()
session.switch = Pyhiveapi.Switch()
session.weather = Pyhiveapi.Weather()
session.attributes = Pyhiveapi.Attributes()
hass.data[DATA_HIVE] = session
for ha_type, hive_type in DEVICETYPES.items():
+74 -58
View File
@@ -3,37 +3,41 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/homekit/
"""
import ipaddress
import logging
from zlib import adler32
import voluptuous as vol
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.cover import SUPPORT_SET_POSITION
from homeassistant.components.cover import (
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.const import (
ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS,
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry
from .const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START)
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE)
from .util import (
validate_entity_config, show_setup_message)
TYPES = Registry()
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['HAP-python==1.1.8']
REQUIREMENTS = ['HAP-python==1.1.9']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS):
vol.All(ipaddress.ip_address, cv.string),
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
@@ -47,11 +51,12 @@ async def async_setup(hass, config):
conf = config[DOMAIN]
port = conf[CONF_PORT]
ip_address = conf.get(CONF_IP_ADDRESS)
auto_start = conf[CONF_AUTO_START]
entity_filter = conf[CONF_FILTER]
entity_config = conf[CONF_ENTITY_CONFIG]
homekit = HomeKit(hass, port, entity_filter, entity_config)
homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config)
homekit.setup()
if auto_start:
@@ -79,55 +84,64 @@ def get_accessory(hass, state, aid, config):
state.entity_id)
return None
if state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
_LOGGER.debug('Add "%s" as "%s"',
state.entity_id, 'TemperatureSensor')
return TYPES['TemperatureSensor'](hass, state.entity_id,
state.name, aid=aid)
elif unit == '%':
_LOGGER.debug('Add "%s" as %s"',
state.entity_id, 'HumiditySensor')
return TYPES['HumiditySensor'](hass, state.entity_id, state.name,
aid=aid)
a_type = None
config = config or {}
elif state.domain == 'cover':
# Only add covers that support set_cover_position
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & SUPPORT_SET_POSITION:
_LOGGER.debug('Add "%s" as "%s"',
state.entity_id, 'WindowCovering')
return TYPES['WindowCovering'](hass, state.entity_id, state.name,
aid=aid)
if state.domain == 'alarm_control_panel':
a_type = 'SecuritySystem'
elif state.domain == 'alarm_control_panel':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
alarm_code=config.get(ATTR_CODE),
aid=aid)
elif state.domain == 'binary_sensor' or state.domain == 'device_tracker':
a_type = 'BinarySensor'
elif state.domain == 'climate':
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
SUPPORT_TARGET_TEMPERATURE_HIGH
# Check if climate device supports auto mode
support_auto = bool(features & support_temp_range)
a_type = 'Thermostat'
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
return TYPES['Thermostat'](hass, state.entity_id,
state.name, support_auto, aid=aid)
elif state.domain == 'cover':
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if device_class == 'garage' and \
features & (SUPPORT_OPEN | SUPPORT_CLOSE):
a_type = 'GarageDoorOpener'
elif features & SUPPORT_SET_POSITION:
a_type = 'WindowCovering'
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
a_type = 'WindowCoveringBasic'
elif state.domain == 'light':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
a_type = 'Light'
elif state.domain == 'lock':
a_type = 'Lock'
elif state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \
or unit == TEMP_FAHRENHEIT:
a_type = 'TemperatureSensor'
elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%':
a_type = 'HumiditySensor'
elif device_class == DEVICE_CLASS_PM25 \
or DEVICE_CLASS_PM25 in state.entity_id:
a_type = 'AirQualitySensor'
elif device_class == DEVICE_CLASS_CO2 \
or DEVICE_CLASS_CO2 in state.entity_id:
a_type = 'CarbonDioxideSensor'
elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \
unit == 'lux':
a_type = 'LightSensor'
elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean' or state.domain == 'script':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
a_type = 'Switch'
return None
if a_type is None:
return None
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config)
def generate_aid(entity_id):
@@ -141,10 +155,11 @@ def generate_aid(entity_id):
class HomeKit():
"""Class to handle all actions between HomeKit and Home Assistant."""
def __init__(self, hass, port, entity_filter, entity_config):
def __init__(self, hass, port, ip_address, entity_filter, entity_config):
"""Initialize a HomeKit object."""
self._hass = hass
self.hass = hass
self._port = port
self._ip_address = ip_address
self._filter = entity_filter
self._config = entity_config
self.started = False
@@ -156,12 +171,13 @@ class HomeKit():
"""Setup bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver
self._hass.bus.async_listen_once(
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop)
path = self._hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self._hass)
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
ip_addr = self._ip_address or get_local_ip()
path = self.hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self.hass)
self.driver = HomeDriver(self.bridge, self._port, ip_addr, path)
def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand."""
@@ -169,7 +185,7 @@ class HomeKit():
return
aid = generate_aid(state.entity_id)
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self._hass, state, aid, conf)
acc = get_accessory(self.hass, state, aid, conf)
if acc is not None:
self.bridge.add_accessory(acc)
@@ -181,15 +197,15 @@ class HomeKit():
# pylint: disable=unused-variable
from . import ( # noqa F401
type_covers, type_lights, type_security_systems, type_sensors,
type_switches, type_thermostats)
type_covers, type_lights, type_locks, type_security_systems,
type_sensors, type_switches, type_thermostats)
for state in self._hass.states.all():
for state in self.hass.states.all():
self.add_bridge_accessory(state)
self.bridge.set_broker(self.driver)
if not self.bridge.paired:
show_setup_message(self.bridge, self._hass)
show_setup_message(self.hass, self.bridge)
_LOGGER.debug('Driver start')
self.driver.start()
+85 -17
View File
@@ -1,21 +1,64 @@
"""Extend the basic Accessory and Bridge functions."""
from datetime import timedelta
from functools import wraps
from inspect import getmodule
import logging
from pyhap.accessory import Accessory, Bridge, Category
from pyhap.accessory_driver import AccessoryDriver
from homeassistant.helpers.event import async_track_state_change
from homeassistant.core import callback as ha_callback
from homeassistant.helpers.event import (
async_track_state_change, track_point_in_utc_time)
from homeassistant.util import dt as dt_util
from .const import (
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
CHAR_NAME, CHAR_SERIAL_NUMBER)
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER,
SERV_ACCESSORY_INFO, CHAR_MANUFACTURER,
CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import (
show_setup_message, dismiss_setup_message)
_LOGGER = logging.getLogger(__name__)
def debounce(func):
"""Decorator function. Debounce callbacks form HomeKit."""
@ha_callback
def call_later_listener(*args):
"""Callback listener called from call_later."""
# pylint: disable=unsubscriptable-object
nonlocal lastargs, remove_listener
hass = lastargs['hass']
hass.async_add_job(func, *lastargs['args'])
lastargs = remove_listener = None
@wraps(func)
def wrapper(*args):
"""Wrapper starts async timer.
The accessory must have 'self.hass' and 'self.entity_id' as attributes.
"""
# pylint: disable=not-callable
hass = args[0].hass
nonlocal lastargs, remove_listener
if remove_listener:
remove_listener()
lastargs = remove_listener = None
lastargs = {'hass': hass, 'args': [*args]}
remove_listener = track_point_in_utc_time(
hass, call_later_listener,
dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT))
logger.debug('%s: Start %s timeout', args[0].entity_id,
func.__name__.replace('set_', ''))
remove_listener = None
lastargs = None
name = getmodule(func).__name__
logger = logging.getLogger(name)
return wrapper
def add_preload_service(acc, service, chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
@@ -29,6 +72,18 @@ def add_preload_service(acc, service, chars=None):
return service
def setup_char(char_name, service, value=None, properties=None, callback=None):
"""Helper function to return fully configured characteristic."""
char = service.get_characteristic(char_name)
if value:
char.value = value
if properties:
char.override_properties(properties)
if callback:
char.setter_callback = callback
return char
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
@@ -42,14 +97,13 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
class HomeAccessory(Accessory):
"""Adapter class for Accessory."""
# pylint: disable=no-member
def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
category='OTHER', **kwargs):
def __init__(self, hass, name, entity_id, aid, category):
"""Initialize a Accessory object."""
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
super().__init__(name, aid=aid)
set_accessory_info(self, name, model=entity_id)
self.category = getattr(Category, category, Category.OTHER)
self.entity_id = entity_id
self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
@@ -57,19 +111,33 @@ class HomeAccessory(Accessory):
def run(self):
"""Method called by accessory after driver is started."""
state = self.hass.states.get(self.entity_id)
self.update_state(new_state=state)
self.update_state_callback(new_state=state)
async_track_state_change(
self.hass, self.entity_id, self.update_state)
self.hass, self.entity_id, self.update_state_callback)
def update_state_callback(self, entity_id=None, old_state=None,
new_state=None):
"""Callback from state change listener."""
_LOGGER.debug('New_state: %s', new_state)
if new_state is None:
return
self.update_state(new_state)
def update_state(self, new_state):
"""Method called on state change to update HomeKit value.
Overridden by accessory types.
"""
pass
class HomeBridge(Bridge):
"""Adapter class for Bridge."""
def __init__(self, hass, name=BRIDGE_NAME,
model=BRIDGE_MODEL, **kwargs):
def __init__(self, hass, name=BRIDGE_NAME):
"""Initialize a Bridge object."""
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
super().__init__(name)
set_accessory_info(self, name, model=BRIDGE_MODEL)
self.hass = hass
def _set_services(self):
@@ -87,7 +155,7 @@ class HomeBridge(Bridge):
def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid)
show_setup_message(self, self.hass)
show_setup_message(self.hass, self)
class HomeDriver(AccessoryDriver):
+51 -7
View File
@@ -1,5 +1,6 @@
"""Constants used be the HomeKit component."""
# #### MISC ####
DEBOUNCE_TIMEOUT = 0.5
DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548
@@ -17,15 +18,15 @@ DEFAULT_PORT = 51827
SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS ####
ACCESSORY_MODEL = 'homekit.accessory'
ACCESSORY_NAME = 'Home Accessory'
BRIDGE_MODEL = 'homekit.bridge'
BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant'
# #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER'
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_LOCK = 'DOOR_LOCK'
CATEGORY_SENSOR = 'SENSOR'
CATEGORY_SWITCH = 'SWITCH'
CATEGORY_THERMOSTAT = 'THERMOSTAT'
@@ -34,40 +35,83 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
# StatusLowBattery, Name
SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
SERV_CONTACT_SENSOR = 'ContactSensor'
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity
SERV_LEAK_SENSOR = 'LeakSensor'
SERV_LIGHT_SENSOR = 'LightSensor'
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
SERV_LOCK = 'LockMechanism'
SERV_MOTION_SENSOR = 'MotionSensor'
SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
SERV_SECURITY_SYSTEM = 'SecuritySystem'
SERV_SMOKE_SENSOR = 'SmokeSensor'
SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering'
# CurrentPosition, TargetPosition, PositionState
# #### Characteristics ####
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
CHAR_AIR_QUALITY = 'AirQuality'
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected'
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition'
CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
CHAR_LEAK_DETECTED = 'LeakDetected'
CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
CHAR_LOCK_TARGET_STATE = 'LockTargetState'
CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model'
CHAR_MOTION_DETECTED = 'MotionDetected'
CHAR_NAME = 'Name'
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState'
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_SMOKE_DETECTED = 'SmokeDetected'
CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
CHAR_TARGET_POSITION = 'TargetPosition'
CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100]
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# #### Properties ####
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
# #### Device Class ####
DEVICE_CLASS_CO2 = 'co2'
DEVICE_CLASS_DOOR = 'door'
DEVICE_CLASS_GARAGE_DOOR = 'garage_door'
DEVICE_CLASS_GAS = 'gas'
DEVICE_CLASS_HUMIDITY = 'humidity'
DEVICE_CLASS_LIGHT = 'light'
DEVICE_CLASS_MOISTURE = 'moisture'
DEVICE_CLASS_MOTION = 'motion'
DEVICE_CLASS_OCCUPANCY = 'occupancy'
DEVICE_CLASS_OPENING = 'opening'
DEVICE_CLASS_PM25 = 'pm25'
DEVICE_CLASS_SMOKE = 'smoke'
DEVICE_CLASS_TEMPERATURE = 'temperature'
DEVICE_CLASS_WINDOW = 'window'
+128 -42
View File
@@ -1,18 +1,67 @@
"""Class to hold all cover accessories."""
import logging
from homeassistant.components.cover import ATTR_CURRENT_POSITION
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED,
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER,
ATTR_SUPPORTED_FEATURES)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
_LOGGER = logging.getLogger(__name__)
@TYPES.register('GarageDoorOpener')
class GarageDoorOpener(HomeAccessory):
"""Generate a Garage Door Opener accessory for a cover entity.
The cover entity must be in the 'garage' device class
and support no more than open, close, and stop.
"""
def __init__(self, *args, config):
"""Initialize a GarageDoorOpener accessory object."""
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
self.flag_target_state = False
serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER)
self.char_current_state = setup_char(
CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0)
self.char_target_state = setup_char(
CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0,
callback=self.set_state)
def set_state(self, value):
"""Change garage state if call came from HomeKit."""
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self.flag_target_state = True
if value == 0:
self.char_current_state.set_value(3)
self.hass.components.cover.open_cover(self.entity_id)
elif value == 1:
self.char_current_state.set_value(2)
self.hass.components.cover.close_cover(self.entity_id)
def update_state(self, new_state):
"""Update cover state after state changed."""
hass_state = new_state.state
if hass_state in (STATE_OPEN, STATE_CLOSED):
current_state = 0 if hass_state == STATE_OPEN else 1
self.char_current_state.set_value(current_state)
if not self.flag_target_state:
self.char_target_state.set_value(current_state)
self.flag_target_state = False
@TYPES.register('WindowCovering')
class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity.
@@ -20,54 +69,91 @@ class WindowCovering(HomeAccessory):
The cover entity must support: set_cover_position.
"""
def __init__(self, hass, entity_id, display_name, **kwargs):
def __init__(self, *args, config):
"""Initialize a WindowCovering accessory object."""
super().__init__(display_name, entity_id,
CATEGORY_WINDOW_COVERING, **kwargs)
self.hass = hass
self.entity_id = entity_id
self.current_position = None
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
self.homekit_target = None
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = serv_cover. \
get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = serv_cover. \
get_characteristic(CHAR_POSITION_STATE)
self.char_current_position.value = 0
self.char_target_position.value = 0
self.char_position_state.value = 0
self.char_target_position.setter_callback = self.move_cover
self.char_current_position = setup_char(
CHAR_CURRENT_POSITION, serv_cover, value=0)
self.char_target_position = setup_char(
CHAR_TARGET_POSITION, serv_cover, value=0,
callback=self.move_cover)
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
self.char_target_position.set_value(value, should_callback=False)
if value != self.current_position:
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
self.homekit_target = value
if value > self.current_position:
self.char_position_state.set_value(1)
elif value < self.current_position:
self.char_position_state.set_value(0)
self.hass.components.cover.set_cover_position(
value, self.entity_id)
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
self.homekit_target = value
def update_state(self, entity_id=None, old_state=None, new_state=None):
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value}
self.hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, params)
def update_state(self, new_state):
"""Update cover position after state changed."""
if new_state is None:
return
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if isinstance(current_position, int):
self.current_position = current_position
self.char_current_position.set_value(self.current_position)
self.char_current_position.set_value(current_position)
if self.homekit_target is None or \
abs(self.current_position - self.homekit_target) < 6:
self.char_target_position.set_value(self.current_position)
self.char_position_state.set_value(2)
abs(current_position - self.homekit_target) < 6:
self.char_target_position.set_value(current_position)
self.homekit_target = None
@TYPES.register('WindowCoveringBasic')
class WindowCoveringBasic(HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: open_cover, close_cover,
stop_cover (optional).
"""
def __init__(self, *args, config):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
self.supports_stop = features & SUPPORT_STOP
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = setup_char(
CHAR_CURRENT_POSITION, serv_cover, value=0)
self.char_target_position = setup_char(
CHAR_TARGET_POSITION, serv_cover, value=0,
callback=self.move_cover)
self.char_position_state = setup_char(
CHAR_POSITION_STATE, serv_cover, value=2)
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
if self.supports_stop:
if value > 70:
service, position = (SERVICE_OPEN_COVER, 100)
elif value < 30:
service, position = (SERVICE_CLOSE_COVER, 0)
else:
service, position = (SERVICE_STOP_COVER, 50)
else:
if value >= 50:
service, position = (SERVICE_OPEN_COVER, 100)
else:
service, position = (SERVICE_CLOSE_COVER, 0)
self.hass.services.call(DOMAIN, service,
{ATTR_ENTITY_ID: self.entity_id})
# Snap the current/target position to the expected final position.
self.char_current_position.set_value(position)
self.char_target_position.set_value(position)
self.char_position_state.set_value(2)
def update_state(self, new_state):
"""Update cover position after state changed."""
position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0}
hk_position = position_mapping.get(new_state.state)
if hk_position is not None:
self.char_current_position.set_value(hk_position)
self.char_target_position.set_value(hk_position)
self.char_position_state.set_value(2)
+27 -45
View File
@@ -7,7 +7,8 @@ from homeassistant.components.light import (
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .accessories import (
HomeAccessory, add_preload_service, debounce, setup_char)
from .const import (
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
@@ -24,12 +25,9 @@ class Light(HomeAccessory):
Currently supports: state, brightness, color temperature, rgb_color.
"""
def __init__(self, hass, entity_id, name, **kwargs):
def __init__(self, *args, config):
"""Initialize a new Light accessory object."""
super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs)
self.hass = hass
self.entity_id = entity_id
super().__init__(*args, category=CATEGORY_LIGHT)
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False,
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
@@ -49,36 +47,29 @@ class Light(HomeAccessory):
self._saturation = None
serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars)
self.char_on = serv_light.get_characteristic(CHAR_ON)
self.char_on.setter_callback = self.set_state
self.char_on.value = self._state
self.char_on = setup_char(
CHAR_ON, serv_light, value=self._state, callback=self.set_state)
if CHAR_BRIGHTNESS in self.chars:
self.char_brightness = serv_light \
.get_characteristic(CHAR_BRIGHTNESS)
self.char_brightness.setter_callback = self.set_brightness
self.char_brightness.value = 0
self.char_brightness = setup_char(
CHAR_BRIGHTNESS, serv_light, value=0,
callback=self.set_brightness)
if CHAR_COLOR_TEMPERATURE in self.chars:
self.char_color_temperature = serv_light \
.get_characteristic(CHAR_COLOR_TEMPERATURE)
self.char_color_temperature.setter_callback = \
self.set_color_temperature
min_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MIN_MIREDS, 153)
max_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MAX_MIREDS, 500)
self.char_color_temperature.override_properties({
'minValue': min_mireds, 'maxValue': max_mireds})
self.char_color_temperature.value = min_mireds
self.char_color_temperature = setup_char(
CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds,
properties={'minValue': min_mireds, 'maxValue': max_mireds},
callback=self.set_color_temperature)
if CHAR_HUE in self.chars:
self.char_hue = serv_light.get_characteristic(CHAR_HUE)
self.char_hue.setter_callback = self.set_hue
self.char_hue.value = 0
self.char_hue = setup_char(
CHAR_HUE, serv_light, value=0, callback=self.set_hue)
if CHAR_SATURATION in self.chars:
self.char_saturation = serv_light \
.get_characteristic(CHAR_SATURATION)
self.char_saturation.setter_callback = self.set_saturation
self.char_saturation.value = 75
self.char_saturation = setup_char(
CHAR_SATURATION, serv_light, value=75,
callback=self.set_saturation)
def set_state(self, value):
"""Set state if call came from HomeKit."""
@@ -87,18 +78,17 @@ class Light(HomeAccessory):
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self._flag[CHAR_ON] = True
self.char_on.set_value(value, should_callback=False)
if value == 1:
self.hass.components.light.turn_on(self.entity_id)
elif value == 0:
self.hass.components.light.turn_off(self.entity_id)
@debounce
def set_brightness(self, value):
"""Set brightness if call came from HomeKit."""
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
self._flag[CHAR_BRIGHTNESS] = True
self.char_brightness.set_value(value, should_callback=False)
if value != 0:
self.hass.components.light.turn_on(
self.entity_id, brightness_pct=value)
@@ -109,14 +99,12 @@ class Light(HomeAccessory):
"""Set color temperature if call came from HomeKit."""
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
self._flag[CHAR_COLOR_TEMPERATURE] = True
self.char_color_temperature.set_value(value, should_callback=False)
self.hass.components.light.turn_on(self.entity_id, color_temp=value)
def set_saturation(self, value):
"""Set saturation if call came from HomeKit."""
_LOGGER.debug('%s: Set saturation to %d', self.entity_id, value)
self._flag[CHAR_SATURATION] = True
self.char_saturation.set_value(value, should_callback=False)
self._saturation = value
self.set_color()
@@ -124,7 +112,6 @@ class Light(HomeAccessory):
"""Set hue if call came from HomeKit."""
_LOGGER.debug('%s: Set hue to %d', self.entity_id, value)
self._flag[CHAR_HUE] = True
self.char_hue.set_value(value, should_callback=False)
self._hue = value
self.set_color()
@@ -140,17 +127,14 @@ class Light(HomeAccessory):
self.hass.components.light.turn_on(
self.entity_id, hs_color=color)
def update_state(self, entity_id=None, old_state=None, new_state=None):
def update_state(self, new_state):
"""Update light after state change."""
if not new_state:
return
# Handle State
state = new_state.state
if state in (STATE_ON, STATE_OFF):
self._state = 1 if state == STATE_ON else 0
if not self._flag[CHAR_ON] and self.char_on.value != self._state:
self.char_on.set_value(self._state, should_callback=False)
self.char_on.set_value(self._state)
self._flag[CHAR_ON] = False
# Handle Brightness
@@ -159,17 +143,16 @@ class Light(HomeAccessory):
if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int):
brightness = round(brightness / 255 * 100, 0)
if self.char_brightness.value != brightness:
self.char_brightness.set_value(brightness,
should_callback=False)
self.char_brightness.set_value(brightness)
self._flag[CHAR_BRIGHTNESS] = False
# Handle color temperature
if CHAR_COLOR_TEMPERATURE in self.chars:
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
if not self._flag[CHAR_COLOR_TEMPERATURE] \
and isinstance(color_temperature, int):
self.char_color_temperature.set_value(color_temperature,
should_callback=False)
and isinstance(color_temperature, int) and \
self.char_color_temperature.value != color_temperature:
self.char_color_temperature.set_value(color_temperature)
self._flag[CHAR_COLOR_TEMPERATURE] = False
# Handle Color
@@ -180,8 +163,7 @@ class Light(HomeAccessory):
hue != self._hue or saturation != self._saturation) and \
isinstance(hue, (int, float)) and \
isinstance(saturation, (int, float)):
self.char_hue.set_value(hue, should_callback=False)
self.char_saturation.set_value(saturation,
should_callback=False)
self.char_hue.set_value(hue)
self.char_saturation.set_value(saturation)
self._hue, self._saturation = (hue, saturation)
self._flag[RGB_COLOR] = False
@@ -0,0 +1,67 @@
"""Class to hold all lock accessories."""
import logging
from homeassistant.components.lock import (
ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
_LOGGER = logging.getLogger(__name__)
HASS_TO_HOMEKIT = {STATE_UNLOCKED: 0,
STATE_LOCKED: 1,
# value 2 is Jammed which hass doesn't have a state for
STATE_UNKNOWN: 3}
HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
STATE_TO_SERVICE = {STATE_LOCKED: 'lock',
STATE_UNLOCKED: 'unlock'}
@TYPES.register('Lock')
class Lock(HomeAccessory):
"""Generate a Lock accessory for a lock entity.
The lock entity must support: unlock and lock.
"""
def __init__(self, *args, config):
"""Initialize a Lock accessory object."""
super().__init__(*args, category=CATEGORY_LOCK)
self.flag_target_state = False
serv_lock_mechanism = add_preload_service(self, SERV_LOCK)
self.char_current_state = setup_char(
CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism,
value=HASS_TO_HOMEKIT[STATE_UNKNOWN])
self.char_target_state = setup_char(
CHAR_LOCK_TARGET_STATE, serv_lock_mechanism,
value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state)
def set_state(self, value):
"""Set lock state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set state to %d", self.entity_id, value)
self.flag_target_state = True
hass_value = HOMEKIT_TO_HASS.get(value)
service = STATE_TO_SERVICE[hass_value]
params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call('lock', service, params)
def update_state(self, new_state):
"""Update lock after state changed."""
hass_state = new_state.state
if hass_state in HASS_TO_HOMEKIT:
current_lock_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_lock_state)
_LOGGER.debug('%s: Updated current state to %s (%d)',
self.entity_id, hass_state, current_lock_state)
# LockTargetState only supports locked and unlocked
if hass_state in (STATE_LOCKED, STATE_UNLOCKED):
if not self.flag_target_state:
self.char_target_state.set_value(current_lock_state)
self.flag_target_state = False
@@ -7,7 +7,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_CODE)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
@@ -27,33 +27,24 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel."""
def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
def __init__(self, *args, config):
"""Initialize a SecuritySystem accessory object."""
super().__init__(display_name, entity_id,
CATEGORY_ALARM_SYSTEM, **kwargs)
self.hass = hass
self.entity_id = entity_id
self._alarm_code = alarm_code
super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
self._alarm_code = config.get(ATTR_CODE)
self.flag_target_state = False
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
self.char_current_state = serv_alarm. \
get_characteristic(CHAR_CURRENT_SECURITY_STATE)
self.char_current_state.value = 3
self.char_target_state = serv_alarm. \
get_characteristic(CHAR_TARGET_SECURITY_STATE)
self.char_target_state.value = 3
self.char_target_state.setter_callback = self.set_security_state
self.char_current_state = setup_char(
CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3)
self.char_target_state = setup_char(
CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3,
callback=self.set_security_state)
def set_security_state(self, value):
"""Move security state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set security state to %d',
self.entity_id, value)
self.flag_target_state = True
self.char_target_state.set_value(value, should_callback=False)
hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value]
@@ -62,23 +53,16 @@ class SecuritySystem(HomeAccessory):
params[ATTR_CODE] = self._alarm_code
self.hass.services.call('alarm_control_panel', service, params)
def update_state(self, entity_id=None, old_state=None, new_state=None):
def update_state(self, new_state):
"""Update security state after state changed."""
if new_state is None:
return
hass_state = new_state.state
if hass_state not in HASS_TO_HOMEKIT:
return
if hass_state in HASS_TO_HOMEKIT:
current_security_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_security_state)
_LOGGER.debug('%s: Updated current state to %s (%d)',
self.entity_id, hass_state, current_security_state)
current_security_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_security_state,
should_callback=False)
_LOGGER.debug('%s: Updated current state to %s (%d)',
self.entity_id, hass_state, current_security_state)
if not self.flag_target_state:
self.char_target_state.set_value(current_security_state,
should_callback=False)
if self.char_target_state.value == self.char_current_state.value:
self.flag_target_state = False
if not self.flag_target_state:
self.char_target_state.set_value(current_security_state)
if self.char_target_state.value == self.char_current_state.value:
self.flag_target_state = False
+141 -33
View File
@@ -2,18 +2,45 @@
import logging
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
from .util import convert_to_float, temperature_to_homekit
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED,
DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR,
CHAR_CARBON_MONOXIDE_DETECTED,
DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED,
DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED,
DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED,
DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE,
DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW,
DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)
from .util import (
convert_to_float, temperature_to_homekit, density_to_air_quality)
_LOGGER = logging.getLogger(__name__)
BINARY_SENSOR_SERVICE_MAP = {
DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR,
CHAR_CARBON_DIOXIDE_DETECTED),
DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE),
DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE),
DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR,
CHAR_CARBON_MONOXIDE_DETECTED),
DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED),
DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED),
DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED),
DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE),
DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED),
DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE)}
@TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory):
@@ -22,29 +49,22 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F.
"""
def __init__(self, hass, entity_id, name, **kwargs):
def __init__(self, *args, config):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
self.hass = hass
self.entity_id = entity_id
super().__init__(*args, category=CATEGORY_SENSOR)
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
self.char_temp.override_properties(properties=PROP_CELSIUS)
self.char_temp.value = 0
self.char_temp = setup_char(
CHAR_CURRENT_TEMPERATURE, serv_temp, value=0,
properties=PROP_CELSIUS)
self.unit = None
def update_state(self, entity_id=None, old_state=None, new_state=None):
def update_state(self, new_state):
"""Update temperature after state changed."""
if new_state is None:
return
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
temperature = convert_to_float(new_state.state)
if temperature:
temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature, should_callback=False)
self.char_temp.set_value(temperature)
_LOGGER.debug('%s: Current temperature set to %d°C',
self.entity_id, temperature)
@@ -53,25 +73,113 @@ class TemperatureSensor(HomeAccessory):
class HumiditySensor(HomeAccessory):
"""Generate a HumiditySensor accessory as humidity sensor."""
def __init__(self, hass, entity_id, name, *args, **kwargs):
def __init__(self, *args, config):
"""Initialize a HumiditySensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
self.hass = hass
self.entity_id = entity_id
super().__init__(*args, category=CATEGORY_SENSOR)
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity \
.get_characteristic(CHAR_CURRENT_HUMIDITY)
self.char_humidity.value = 0
self.char_humidity = setup_char(
CHAR_CURRENT_HUMIDITY, serv_humidity, value=0)
def update_state(self, entity_id=None, old_state=None, new_state=None):
def update_state(self, new_state):
"""Update accessory after state change."""
if new_state is None:
return
humidity = convert_to_float(new_state.state)
if humidity:
self.char_humidity.set_value(humidity, should_callback=False)
self.char_humidity.set_value(humidity)
_LOGGER.debug('%s: Percent set to %d%%',
self.entity_id, humidity)
@TYPES.register('AirQualitySensor')
class AirQualitySensor(HomeAccessory):
"""Generate a AirQualitySensor accessory as air quality sensor."""
def __init__(self, *args, config):
"""Initialize a AirQualitySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR,
[CHAR_AIR_PARTICULATE_DENSITY])
self.char_quality = setup_char(
CHAR_AIR_QUALITY, serv_air_quality, value=0)
self.char_density = setup_char(
CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
density = convert_to_float(new_state.state)
if density is not None:
self.char_density.set_value(density)
self.char_quality.set_value(density_to_air_quality(density))
_LOGGER.debug('%s: Set to %d', self.entity_id, density)
@TYPES.register('CarbonDioxideSensor')
class CarbonDioxideSensor(HomeAccessory):
"""Generate a CarbonDioxideSensor accessory as CO2 sensor."""
def __init__(self, *args, config):
"""Initialize a CarbonDioxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [
CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
self.char_co2 = setup_char(
CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0)
self.char_peak = setup_char(
CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0)
self.char_detected = setup_char(
CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
co2 = convert_to_float(new_state.state)
if co2 is not None:
self.char_co2.set_value(co2)
if co2 > self.char_peak.value:
self.char_peak.set_value(co2)
self.char_detected.set_value(co2 > 1000)
_LOGGER.debug('%s: Set to %d', self.entity_id, co2)
@TYPES.register('LightSensor')
class LightSensor(HomeAccessory):
"""Generate a LightSensor accessory as light sensor."""
def __init__(self, *args, config):
"""Initialize a LightSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
serv_light = add_preload_service(self, SERV_LIGHT_SENSOR)
self.char_light = setup_char(
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
luminance = convert_to_float(new_state.state)
if luminance is not None:
self.char_light.set_value(luminance)
_LOGGER.debug('%s: Set to %d', self.entity_id, luminance)
@TYPES.register('BinarySensor')
class BinarySensor(HomeAccessory):
"""Generate a BinarySensor accessory as binary sensor."""
def __init__(self, *args, config):
"""Initialize a BinarySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
device_class = self.hass.states.get(self.entity_id).attributes \
.get(ATTR_DEVICE_CLASS)
service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \
if device_class in BINARY_SENSOR_SERVICE_MAP \
else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY]
service = add_preload_service(self, service_char[0])
self.char_detected = setup_char(service_char[1], service, value=0)
def update_state(self, new_state):
"""Update accessory after state change."""
state = new_state.state
detected = (state == STATE_ON) or (state == STATE_HOME)
self.char_detected.set_value(detected)
_LOGGER.debug('%s: Set to %d', self.entity_id, detected)
@@ -6,7 +6,7 @@ from homeassistant.const import (
from homeassistant.core import split_entity_id
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__)
@@ -16,40 +16,30 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory):
"""Generate a Switch accessory."""
def __init__(self, hass, entity_id, display_name, **kwargs):
def __init__(self, *args, config):
"""Initialize a Switch accessory object to represent a remote."""
super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs)
self.hass = hass
self.entity_id = entity_id
self._domain = split_entity_id(entity_id)[0]
super().__init__(*args, category=CATEGORY_SWITCH)
self._domain = split_entity_id(self.entity_id)[0]
self.flag_target_state = False
serv_switch = add_preload_service(self, SERV_SWITCH)
self.char_on = serv_switch.get_characteristic(CHAR_ON)
self.char_on.value = False
self.char_on.setter_callback = self.set_state
self.char_on = setup_char(
CHAR_ON, serv_switch, value=False, callback=self.set_state)
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s',
self.entity_id, value)
self.flag_target_state = True
self.char_on.set_value(value, should_callback=False)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self.entity_id})
def update_state(self, entity_id=None, old_state=None, new_state=None):
def update_state(self, new_state):
"""Update switch state after state changed."""
if new_state is None:
return
current_state = (new_state.state == STATE_ON)
if not self.flag_target_state:
_LOGGER.debug('%s: Set current state to %s',
self.entity_id, current_state)
self.char_on.set_value(current_state, should_callback=False)
self.char_on.set_value(current_state)
self.flag_target_state = False
@@ -5,12 +5,15 @@ from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO)
STATE_HEAT, STATE_COOL, STATE_AUTO,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .accessories import (
HomeAccessory, add_preload_service, debounce, setup_char)
from .const import (
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
@@ -26,78 +29,66 @@ HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
STATE_COOL: 2, STATE_AUTO: 3}
HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \
SUPPORT_TARGET_TEMPERATURE_HIGH
@TYPES.register('Thermostat')
class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate."""
def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
def __init__(self, *args, config):
"""Initialize a Thermostat accessory object."""
super().__init__(display_name, entity_id,
CATEGORY_THERMOSTAT, **kwargs)
self.hass = hass
self.entity_id = entity_id
self._call_timer = None
super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = TEMP_CELSIUS
self.heat_cool_flag_target_state = False
self.temperature_flag_target_state = False
self.coolingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False
# Add additional characteristics if auto mode is supported
extra_chars = [
CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None
self.chars = []
features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
if features & SUPPORT_TEMP_RANGE:
self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE))
# Preload the thermostat service
serv_thermostat = add_preload_service(self, SERV_THERMOSTAT,
extra_chars)
serv_thermostat = add_preload_service(
self, SERV_THERMOSTAT, self.chars)
# Current and target mode characteristics
self.char_current_heat_cool = serv_thermostat. \
get_characteristic(CHAR_CURRENT_HEATING_COOLING)
self.char_current_heat_cool.value = 0
self.char_target_heat_cool = serv_thermostat. \
get_characteristic(CHAR_TARGET_HEATING_COOLING)
self.char_target_heat_cool.value = 0
self.char_target_heat_cool.setter_callback = self.set_heat_cool
self.char_current_heat_cool = setup_char(
CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0)
self.char_target_heat_cool = setup_char(
CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0,
callback=self.set_heat_cool)
# Current and target temperature characteristics
self.char_current_temp = serv_thermostat. \
get_characteristic(CHAR_CURRENT_TEMPERATURE)
self.char_current_temp.value = 21.0
self.char_target_temp = serv_thermostat. \
get_characteristic(CHAR_TARGET_TEMPERATURE)
self.char_target_temp.value = 21.0
self.char_target_temp.setter_callback = self.set_target_temperature
self.char_current_temp = setup_char(
CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0)
self.char_target_temp = setup_char(
CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0,
callback=self.set_target_temperature)
# Display units characteristic
self.char_display_units = serv_thermostat. \
get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
self.char_display_units.value = 0
self.char_display_units = setup_char(
CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0)
# If the device supports it: high and low temperature characteristics
if support_auto:
self.char_cooling_thresh_temp = serv_thermostat. \
get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
self.char_cooling_thresh_temp.value = 23.0
self.char_cooling_thresh_temp.setter_callback = \
self.set_cooling_threshold
self.char_heating_thresh_temp = serv_thermostat. \
get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
self.char_heating_thresh_temp.value = 19.0
self.char_heating_thresh_temp.setter_callback = \
self.set_heating_threshold
else:
self.char_cooling_thresh_temp = None
self.char_heating_thresh_temp = None
self.char_cooling_thresh_temp = None
self.char_heating_thresh_temp = None
if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
self.char_cooling_thresh_temp = setup_char(
CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat,
value=23.0, callback=self.set_cooling_threshold)
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
self.char_heating_thresh_temp = setup_char(
CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat,
value=19.0, callback=self.set_heating_threshold)
def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit."""
self.char_target_heat_cool.set_value(value, should_callback=False)
if value in HC_HOMEKIT_TO_HASS:
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.heat_cool_flag_target_state = True
@@ -105,12 +96,12 @@ class Thermostat(HomeAccessory):
self.hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self.entity_id)
@debounce
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
self.entity_id, value)
self.coolingthresh_flag_target_state = True
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
low = self.char_heating_thresh_temp.value
low = temperature_to_states(low, self._unit)
value = temperature_to_states(value, self._unit)
@@ -118,12 +109,12 @@ class Thermostat(HomeAccessory):
entity_id=self.entity_id, target_temp_high=value,
target_temp_low=low)
@debounce
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
self.entity_id, value)
self.heatingthresh_flag_target_state = True
self.char_heating_thresh_temp.set_value(value, should_callback=False)
# Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit)
@@ -132,21 +123,18 @@ class Thermostat(HomeAccessory):
entity_id=self.entity_id, target_temp_high=high,
target_temp_low=value)
@debounce
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f°C',
self.entity_id, value)
self.temperature_flag_target_state = True
self.char_target_temp.set_value(value, should_callback=False)
value = temperature_to_states(value, self._unit)
self.hass.components.climate.set_temperature(
temperature=value, entity_id=self.entity_id)
def update_state(self, entity_id=None, old_state=None, new_state=None):
def update_state(self, new_state):
"""Update security state after state changed."""
if new_state is None:
return
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS)
@@ -161,8 +149,7 @@ class Thermostat(HomeAccessory):
if isinstance(target_temp, (int, float)):
target_temp = temperature_to_homekit(target_temp, self._unit)
if not self.temperature_flag_target_state:
self.char_target_temp.set_value(target_temp,
should_callback=False)
self.char_target_temp.set_value(target_temp)
self.temperature_flag_target_state = False
# Update cooling threshold temperature if characteristic exists
@@ -172,8 +159,7 @@ class Thermostat(HomeAccessory):
cooling_thresh = temperature_to_homekit(cooling_thresh,
self._unit)
if not self.coolingthresh_flag_target_state:
self.char_cooling_thresh_temp.set_value(
cooling_thresh, should_callback=False)
self.char_cooling_thresh_temp.set_value(cooling_thresh)
self.coolingthresh_flag_target_state = False
# Update heating threshold temperature if characteristic exists
@@ -183,8 +169,7 @@ class Thermostat(HomeAccessory):
heating_thresh = temperature_to_homekit(heating_thresh,
self._unit)
if not self.heatingthresh_flag_target_state:
self.char_heating_thresh_temp.set_value(
heating_thresh, should_callback=False)
self.char_heating_thresh_temp.set_value(heating_thresh)
self.heatingthresh_flag_target_state = False
# Update display units
@@ -197,7 +182,7 @@ class Thermostat(HomeAccessory):
and operation_mode in HC_HASS_TO_HOMEKIT:
if not self.heat_cool_flag_target_state:
self.char_target_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
HC_HASS_TO_HOMEKIT[operation_mode])
self.heat_cool_flag_target_state = False
# Set current operation mode based on temperatures and target mode
+14 -1
View File
@@ -33,7 +33,7 @@ def validate_entity_config(values):
return entities
def show_setup_message(bridge, hass):
def show_setup_message(hass, bridge):
"""Display persistent notification with setup information."""
pin = bridge.pincode.decode()
_LOGGER.info('Pincode: %s', pin)
@@ -64,3 +64,16 @@ def temperature_to_homekit(temperature, unit):
def temperature_to_states(temperature, unit):
"""Convert temperature back from Celsius to Home Assistant unit."""
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1)
def density_to_air_quality(density):
"""Map PM2.5 density to HomeKit AirQuality level."""
if density <= 35:
return 1
elif density <= 75:
return 2
elif density <= 115:
return 3
elif density <= 150:
return 4
return 5
@@ -0,0 +1,249 @@
"""
Support for Homekit device discovery.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homekit_controller/
"""
import http
import json
import logging
import os
import uuid
from homeassistant.components.discovery import SERVICE_HOMEKIT
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['homekit==0.6']
DOMAIN = 'homekit_controller'
HOMEKIT_DIR = '.homekit'
# Mapping from Homekit type to component.
HOMEKIT_ACCESSORY_DISPATCH = {
'lightbulb': 'light',
'outlet': 'switch',
}
KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
_LOGGER = logging.getLogger(__name__)
def homekit_http_send(self, message_body=None, encode_chunked=False):
r"""Send the currently buffered request and clear the buffer.
Appends an extra \r\n to the buffer.
A message_body may be specified, to be appended to the request.
"""
self._buffer.extend((b"", b""))
msg = b"\r\n".join(self._buffer)
del self._buffer[:]
if message_body is not None:
msg = msg + message_body
self.send(msg)
def get_serial(accessory):
"""Obtain the serial number of a HomeKit device."""
# pylint: disable=import-error
import homekit
for service in accessory['services']:
if homekit.ServicesTypes.get_short(service['type']) != \
'accessory-information':
continue
for characteristic in service['characteristics']:
ctype = homekit.CharacteristicsTypes.get_short(
characteristic['type'])
if ctype != 'serial-number':
continue
return characteristic['value']
return None
class HKDevice():
"""HomeKit device."""
def __init__(self, hass, host, port, model, hkid, config_num, config):
"""Initialise a generic HomeKit device."""
# pylint: disable=import-error
import homekit
_LOGGER.info("Setting up Homekit device %s", model)
self.hass = hass
self.host = host
self.port = port
self.model = model
self.hkid = hkid
self.config_num = config_num
self.config = config
self.configurator = hass.components.configurator
data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR)
if not os.path.isdir(data_dir):
os.mkdir(data_dir)
self.pairing_file = os.path.join(data_dir, 'hk-{}'.format(hkid))
self.pairing_data = homekit.load_pairing(self.pairing_file)
# Monkey patch httpclient for increased compatibility
# pylint: disable=protected-access
http.client.HTTPConnection._send_output = homekit_http_send
self.conn = http.client.HTTPConnection(self.host, port=self.port)
if self.pairing_data is not None:
self.accessory_setup()
else:
self.configure()
def accessory_setup(self):
"""Handle setup of a HomeKit accessory."""
# pylint: disable=import-error
import homekit
self.controllerkey, self.accessorykey = \
homekit.get_session_keys(self.conn, self.pairing_data)
self.securecon = homekit.SecureHttp(self.conn.sock,
self.accessorykey,
self.controllerkey)
response = self.securecon.get('/accessories')
data = json.loads(response.read().decode())
for accessory in data['accessories']:
serial = get_serial(accessory)
if serial in self.hass.data[KNOWN_ACCESSORIES]:
continue
self.hass.data[KNOWN_ACCESSORIES][serial] = self
aid = accessory['aid']
for service in accessory['services']:
service_info = {'serial': serial,
'aid': aid,
'iid': service['iid']}
devtype = homekit.ServicesTypes.get_short(service['type'])
_LOGGER.debug("Found %s", devtype)
component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None)
if component is not None:
discovery.load_platform(self.hass, component, DOMAIN,
service_info, self.config)
def device_config_callback(self, callback_data):
"""Handle initial pairing."""
# pylint: disable=import-error
import homekit
pairing_id = str(uuid.uuid4())
code = callback_data.get('code').strip()
try:
self.pairing_data = homekit.perform_pair_setup(self.conn, code,
pairing_id)
except homekit.exception.UnavailableError:
error_msg = "This accessory is already paired to another device. \
Please reset the accessory and try again."
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.notify_errors(_configurator, error_msg)
return
except homekit.exception.AuthenticationError:
error_msg = "Incorrect HomeKit code for {}. Please check it and \
try again.".format(self.model)
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.notify_errors(_configurator, error_msg)
return
except homekit.exception.UnknownError:
error_msg = "Received an unknown error. Please file a bug."
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.notify_errors(_configurator, error_msg)
raise
if self.pairing_data is not None:
homekit.save_pairing(self.pairing_file, self.pairing_data)
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.request_done(_configurator)
self.accessory_setup()
else:
error_msg = "Unable to pair, please try again"
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.notify_errors(_configurator, error_msg)
def configure(self):
"""Obtain the pairing code for a HomeKit device."""
description = "Please enter the HomeKit code for your {}".format(
self.model)
self.hass.data[DOMAIN+self.hkid] = \
self.configurator.request_config(self.model,
self.device_config_callback,
description=description,
submit_caption="submit",
fields=[{'id': 'code',
'name': 'HomeKit code',
'type': 'string'}])
class HomeKitEntity(Entity):
"""Representation of a Home Assistant HomeKit device."""
def __init__(self, accessory, devinfo):
"""Initialise a generic HomeKit device."""
self._name = accessory.model
self._securecon = accessory.securecon
self._aid = devinfo['aid']
self._iid = devinfo['iid']
self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
self._features = 0
self._chars = {}
def update(self):
"""Obtain a HomeKit device's state."""
response = self._securecon.get('/accessories')
data = json.loads(response.read().decode())
for accessory in data['accessories']:
if accessory['aid'] != self._aid:
continue
for service in accessory['services']:
if service['iid'] != self._iid:
continue
self.update_characteristics(service['characteristics'])
break
@property
def unique_id(self):
"""Return the ID of this device."""
return self._address
@property
def name(self):
"""Return the name of the device if any."""
return self._name
def update_characteristics(self, characteristics):
"""Synchronise a HomeKit device state with Home Assistant."""
raise NotImplementedError
# pylint: too-many-function-args
def setup(hass, config):
"""Set up for Homekit devices."""
def discovery_dispatch(service, discovery_info):
"""Dispatcher for Homekit discovery events."""
# model, id
host = discovery_info['host']
port = discovery_info['port']
model = discovery_info['properties']['md']
hkid = discovery_info['properties']['id']
config_num = int(discovery_info['properties']['c#'])
# Only register a device once, but rescan if the config has changed
if hkid in hass.data[KNOWN_DEVICES]:
device = hass.data[KNOWN_DEVICES][hkid]
if config_num > device.config_num and \
device.pairing_info is not None:
device.accessory_setup()
return
_LOGGER.debug('Discovered unique device %s', hkid)
device = HKDevice(hass, host, port, model, hkid, config_num, config)
hass.data[KNOWN_DEVICES][hkid] = device
hass.data[KNOWN_ACCESSORIES] = {}
hass.data[KNOWN_DEVICES] = {}
discovery.listen(hass, SERVICE_HOMEKIT, discovery_dispatch)
return True
@@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.40']
REQUIREMENTS = ['pyhomematic==0.1.41']
DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__)
@@ -69,7 +69,8 @@ HM_DEVICE_TYPES = {
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'],
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
'IPWeatherSensor'],
DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@@ -78,7 +79,7 @@ HM_DEVICE_TYPES = {
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
'WiredSensor', 'PresenceIP'],
'WiredSensor', 'PresenceIP', 'IPWeatherSensor'],
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
DISCOVER_LOCKS: ['KeyMatic']
}
@@ -89,7 +90,7 @@ HM_IGNORE_DISCOVERY_NODE = [
]
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
'ACTUAL_TEMPERATURE': ['IPAreaThermostat'],
'ACTUAL_TEMPERATURE': ['IPAreaThermostat', 'IPWeatherSensor'],
}
HM_ATTRIBUTE_SUPPORT = {
+6
View File
@@ -131,3 +131,9 @@ 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()
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
bridge = hass.data[DOMAIN].pop(entry.data['host'])
return await bridge.async_reset()
+37 -10
View File
@@ -30,6 +30,7 @@ class HueBridge(object):
self.allow_groups = allow_groups
self.available = True
self.api = None
self._cancel_retry_setup = None
@property
def host(self):
@@ -39,18 +40,17 @@ class HueBridge(object):
async def async_setup(self, tries=0):
"""Set up a phue bridge based on host parameter."""
host = self.host
hass = self.hass
try:
self.api = await get_bridge(
self.hass, host,
self.config_entry.data['username']
)
hass, host, self.config_entry.data['username'])
except AuthenticationRequired:
# usernames can become invalid if hub is reset or user removed.
# We are going to fail the config entry setup and initiate a new
# linking procedure. When linking succeeds, it will remove the
# old config entry.
self.hass.async_add_job(self.hass.config_entries.flow.async_init(
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
'host': host,
}
@@ -68,8 +68,8 @@ class HueBridge(object):
# This feels hacky, we should find a better way to do this
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
# Unhandled edge case: cancel this if we discover bridge on new IP
self.hass.helpers.event.async_call_later(retry_delay, retry_setup)
self._cancel_retry_setup = hass.helpers.event.async_call_later(
retry_delay, retry_setup)
return False
@@ -78,16 +78,43 @@ class HueBridge(object):
host)
return False
self.hass.async_add_job(
self.hass.helpers.discovery.async_load_platform(
'light', DOMAIN, {'host': host}))
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
self.config_entry, 'light'))
self.hass.services.async_register(
hass.services.async_register(
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
schema=SCENE_SCHEMA)
return True
async def async_reset(self):
"""Reset this bridge to default state.
Will cancel any scheduled setup retry and will unload
the config entry.
"""
# The bridge can be in 3 states:
# - Setup was successful, self.api is not None
# - Authentication was wrong, self.api is None, not retrying setup.
# - Host was down. self.api is None, we're retrying setup
# If we have a retry scheduled, we were never setup.
if self._cancel_retry_setup is not None:
self._cancel_retry_setup()
self._cancel_retry_setup = None
return True
# If the authentication was wrong.
if self.api is None:
return True
self.hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE)
# If setup was successful, we set api variable, forwarded entry and
# register service
return await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'light')
async def hue_activate_scene(self, call, updated=False):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
+2 -2
View File
@@ -6,7 +6,7 @@ import os
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant import config_entries, data_entry_flow
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
@@ -41,7 +41,7 @@ def _find_username_from_config(hass, filename):
@config_entries.HANDLERS.register(DOMAIN)
class HueFlowHandler(config_entries.ConfigFlowHandler):
class HueFlowHandler(data_entry_flow.FlowHandler):
"""Handle a Hue config flow."""
VERSION = 1
+15 -14
View File
@@ -1,4 +1,5 @@
"""IHC component.
"""
Support for IHC devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ihc/
@@ -6,18 +7,18 @@ https://home-assistant.io/components/ihc/
import logging
import os.path
import xml.etree.ElementTree
import voluptuous as vol
from homeassistant.components.ihc.const import (
ATTR_IHC_ID, ATTR_VALUE, CONF_INFO, CONF_AUTOSETUP,
CONF_BINARY_SENSOR, CONF_LIGHT, CONF_SENSOR, CONF_SWITCH,
CONF_XPATH, CONF_NODE, CONF_DIMMABLE, CONF_INVERTING,
SERVICE_SET_RUNTIME_VALUE_BOOL, SERVICE_SET_RUNTIME_VALUE_INT,
SERVICE_SET_RUNTIME_VALUE_FLOAT)
ATTR_IHC_ID, ATTR_VALUE, CONF_AUTOSETUP, CONF_BINARY_SENSOR, CONF_DIMMABLE,
CONF_INFO, CONF_INVERTING, CONF_LIGHT, CONF_NODE, CONF_SENSOR, CONF_SWITCH,
CONF_XPATH, SERVICE_SET_RUNTIME_VALUE_BOOL,
SERVICE_SET_RUNTIME_VALUE_FLOAT, SERVICE_SET_RUNTIME_VALUE_INT)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
CONF_URL, CONF_USERNAME, CONF_PASSWORD, CONF_ID, CONF_NAME,
CONF_UNIT_OF_MEASUREMENT, CONF_TYPE, TEMP_CELSIUS)
CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT,
CONF_URL, CONF_USERNAME, TEMP_CELSIUS)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
@@ -36,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean,
vol.Optional(CONF_INFO, default=True): cv.boolean
vol.Optional(CONF_INFO, default=True): cv.boolean,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -97,7 +98,7 @@ IHC_PLATFORMS = ('binary_sensor', 'light', 'sensor', 'switch')
def setup(hass, config):
"""Setup the IHC component."""
"""Set up the IHC component."""
from ihcsdk.ihccontroller import IHCController
conf = config[DOMAIN]
url = conf[CONF_URL]
@@ -106,7 +107,7 @@ def setup(hass, config):
ihc_controller = IHCController(url, username, password)
if not ihc_controller.authenticate():
_LOGGER.error("Unable to authenticate on ihc controller.")
_LOGGER.error("Unable to authenticate on IHC controller")
return False
if (conf[CONF_AUTOSETUP] and
@@ -125,7 +126,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller):
"""Auto setup of IHC products from the ihc project file."""
project_xml = ihc_controller.get_project()
if not project_xml:
_LOGGER.error("Unable to read project from ihc controller.")
_LOGGER.error("Unable to read project from ICH controller")
return False
project = xml.etree.ElementTree.fromstring(project_xml)
@@ -150,7 +151,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller):
def get_discovery_info(component_setup, groups):
"""Get discovery info for specified component."""
"""Get discovery info for specified IHC component."""
discovery_data = {}
for group in groups:
groupname = group.attrib['name']
@@ -173,7 +174,7 @@ def get_discovery_info(component_setup, groups):
def setup_service_functions(hass: HomeAssistantType, ihc_controller):
"""Setup the ihc service functions."""
"""Setup the IHC service functions."""
def set_runtime_value_bool(call):
"""Set a IHC runtime bool value service function."""
ihc_id = call.data[ATTR_IHC_ID]
+5 -5
View File
@@ -1,4 +1,4 @@
"""Implements a base class for all IHC devices."""
"""Implementation of a base class for all IHC devices."""
import asyncio
from xml.etree.ElementTree import Element
@@ -6,7 +6,7 @@ from homeassistant.helpers.entity import Entity
class IHCDevice(Entity):
"""Base class for all ihc devices.
"""Base class for all IHC devices.
All IHC devices have an associated IHC resource. IHCDevice handled the
registration of the IHC controller callback when the IHC resource changes.
@@ -31,13 +31,13 @@ class IHCDevice(Entity):
@asyncio.coroutine
def async_added_to_hass(self):
"""Add callback for ihc changes."""
"""Add callback for IHC changes."""
self.ihc_controller.add_notify_event(
self.ihc_id, self.on_ihc_change, True)
@property
def should_poll(self) -> bool:
"""No polling needed for ihc devices."""
"""No polling needed for IHC devices."""
return False
@property
@@ -58,7 +58,7 @@ class IHCDevice(Entity):
}
def on_ihc_change(self, ihc_id, value):
"""Callback when ihc resource changes.
"""Callback when IHC resource changes.
Derived classes must overwrite this to do device specific stuff.
"""
+11 -1
View File
@@ -334,7 +334,7 @@ class SetIntentHandler(intent.IntentHandler):
async def async_setup(hass, config):
"""Expose light control via state machine and services."""
component = EntityComponent(
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
await component.async_setup(config)
@@ -388,6 +388,16 @@ async def async_setup(hass, config):
return True
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class Profiles:
"""Representation of available color profiles."""
+10 -3
View File
@@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.abode/
"""
import logging
from math import ceil
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_HS_COLOR,
@@ -51,7 +51,9 @@ class AbodeLight(AbodeDevice, Light):
*kwargs[ATTR_HS_COLOR]))
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
self._device.set_level(kwargs[ATTR_BRIGHTNESS])
# Convert HASS brightness (0-255) to Abode brightness (0-99)
# If 100 is sent to Abode, response is 99 causing an error
self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0))
else:
self._device.switch_on()
@@ -68,7 +70,12 @@ class AbodeLight(AbodeDevice, Light):
def brightness(self):
"""Return the brightness of the light."""
if self._device.is_dimmable and self._device.has_brightness:
return self._device.brightness
brightness = int(self._device.brightness)
# Abode returns 100 during device initialization and device refresh
if brightness == 100:
return 255
# Convert Abode brightness (0-99) to HASS brightness (0-255)
return ceil(brightness * 255 / 99.0)
@property
def hs_color(self):
+171
View File
@@ -0,0 +1,171 @@
"""
Support for Eufy lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.eufy/
"""
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light)
import homeassistant.util.color as color_util
from homeassistant.util.color import (
color_temperature_mired_to_kelvin as mired_to_kelvin,
color_temperature_kelvin_to_mired as kelvin_to_mired)
DEPENDENCIES = ['eufy']
_LOGGER = logging.getLogger(__name__)
EUFY_MAX_KELVIN = 6500
EUFY_MIN_KELVIN = 2700
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Eufy bulbs."""
if discovery_info is None:
return
add_devices([EufyLight(discovery_info)], True)
class EufyLight(Light):
"""Representation of a Eufy light."""
def __init__(self, device):
"""Initialize the light."""
# pylint: disable=import-error
import lakeside
self._temp = None
self._brightness = None
self._hs = None
self._state = None
self._name = device['name']
self._address = device['address']
self._code = device['code']
self._type = device['type']
self._bulb = lakeside.bulb(self._address, self._code, self._type)
self._colormode = False
if self._type == "T1011":
self._features = SUPPORT_BRIGHTNESS
elif self._type == "T1012":
self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
elif self._type == "T1013":
self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | \
SUPPORT_COLOR
self._bulb.connect()
def update(self):
"""Synchronise state from the bulb."""
self._bulb.update()
if self._bulb.power:
self._brightness = self._bulb.brightness
self._temp = self._bulb.temperature
if self._bulb.colors:
self._colormode = True
self._hs = color_util.color_RGB_to_hs(*self._bulb.colors)
else:
self._colormode = False
self._state = self._bulb.power
@property
def unique_id(self):
"""Return the ID of this light."""
return self._address
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._state
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return int(self._brightness * 255 / 100)
@property
def min_mireds(self):
"""Return minimum supported color temperature."""
return kelvin_to_mired(EUFY_MAX_KELVIN)
@property
def max_mireds(self):
"""Return maximu supported color temperature."""
return kelvin_to_mired(EUFY_MIN_KELVIN)
@property
def color_temp(self):
"""Return the color temperature of this light."""
temp_in_k = int(EUFY_MIN_KELVIN + (self._temp *
(EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)
/ 100))
return kelvin_to_mired(temp_in_k)
@property
def hs_color(self):
"""Return the color of this light."""
if not self._colormode:
return None
return self._hs
@property
def supported_features(self):
"""Flag supported features."""
return self._features
def turn_on(self, **kwargs):
"""Turn the specified light on."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
colortemp = kwargs.get(ATTR_COLOR_TEMP)
# pylint: disable=invalid-name
hs = kwargs.get(ATTR_HS_COLOR)
if brightness is not None:
brightness = int(brightness * 100 / 255)
else:
if self._brightness is None:
self._brightness = 100
brightness = self._brightness
if colortemp is not None:
self._colormode = False
temp_in_k = mired_to_kelvin(colortemp)
relative_temp = temp_in_k - EUFY_MIN_KELVIN
temp = int(relative_temp * 100 /
(EUFY_MAX_KELVIN - EUFY_MIN_KELVIN))
else:
temp = None
if hs is not None:
rgb = color_util.color_hsv_to_RGB(
hs[0], hs[1], brightness / 255 * 100)
self._colormode = True
elif self._colormode:
rgb = color_util.color_hsv_to_RGB(
self._hs[0], self._hs[1], brightness / 255 * 100)
else:
rgb = None
try:
self._bulb.set_state(power=True, brightness=brightness,
temperature=temp, colors=rgb)
except BrokenPipeError:
self._bulb.connect()
self._bulb.set_state(power=True, brightness=brightness,
temperature=temp, colors=rgb)
def turn_off(self, **kwargs):
"""Turn the specified light off."""
try:
self._bulb.set_state(power=False)
except BrokenPipeError:
self._bulb.connect()
self._bulb.set_state(power=False)
+8
View File
@@ -34,6 +34,7 @@ class HiveDeviceLight(Light):
self.device_type = hivedevice["HA_DeviceType"]
self.light_device_type = hivedevice["Hive_Light_DeviceType"]
self.session = hivesession
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
self.session.entities.append(self)
@@ -48,6 +49,11 @@ class HiveDeviceLight(Light):
"""Return the display name of this light."""
return self.node_name
@property
def device_state_attributes(self):
"""Show Device Attributes."""
return self.attributes
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
@@ -136,3 +142,5 @@ class HiveDeviceLight(Light):
def update(self):
"""Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
self.attributes = self.session.attributes.state_attributes(
self.node_id)
@@ -0,0 +1,134 @@
"""
Support for Homekit lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.homekit_controller/
"""
import json
import logging
from homeassistant.components.homekit_controller import (
HomeKitEntity, KNOWN_ACCESSORIES)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
DEPENDENCIES = ['homekit_controller']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Homekit lighting."""
if discovery_info is not None:
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
add_devices([HomeKitLight(accessory, discovery_info)], True)
class HomeKitLight(HomeKitEntity, Light):
"""Representation of a Homekit light."""
def __init__(self, *args):
"""Initialise the light."""
super().__init__(*args)
self._on = None
self._brightness = None
self._color_temperature = None
self._hue = None
self._saturation = None
def update_characteristics(self, characteristics):
"""Synchronise light state with Home Assistant."""
# pylint: disable=import-error
import homekit
for characteristic in characteristics:
ctype = characteristic['type']
ctype = homekit.CharacteristicsTypes.get_short(ctype)
if ctype == "on":
self._chars['on'] = characteristic['iid']
self._on = characteristic['value']
elif ctype == 'brightness':
self._chars['brightness'] = characteristic['iid']
self._features |= SUPPORT_BRIGHTNESS
self._brightness = characteristic['value']
elif ctype == 'color-temperature':
self._chars['color_temperature'] = characteristic['iid']
self._features |= SUPPORT_COLOR_TEMP
self._color_temperature = characteristic['value']
elif ctype == "hue":
self._chars['hue'] = characteristic['iid']
self._features |= SUPPORT_COLOR
self._hue = characteristic['value']
elif ctype == "saturation":
self._chars['saturation'] = characteristic['iid']
self._features |= SUPPORT_COLOR
self._saturation = characteristic['value']
@property
def is_on(self):
"""Return true if device is on."""
return self._on
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
if self._features & SUPPORT_BRIGHTNESS:
return self._brightness * 255 / 100
return None
@property
def hs_color(self):
"""Return the color property."""
if self._features & SUPPORT_COLOR:
return (self._hue, self._saturation)
return None
@property
def color_temp(self):
"""Return the color temperature."""
if self._features & SUPPORT_COLOR_TEMP:
return self._color_temperature
return None
@property
def supported_features(self):
"""Flag supported features."""
return self._features
def turn_on(self, **kwargs):
"""Turn the specified light on."""
hs_color = kwargs.get(ATTR_HS_COLOR)
temperature = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS)
characteristics = []
if hs_color is not None:
characteristics.append({'aid': self._aid,
'iid': self._chars['hue'],
'value': hs_color[0]})
characteristics.append({'aid': self._aid,
'iid': self._chars['saturation'],
'value': hs_color[1]})
if brightness is not None:
characteristics.append({'aid': self._aid,
'iid': self._chars['brightness'],
'value': int(brightness * 100 / 255)})
if temperature is not None:
characteristics.append({'aid': self._aid,
'iid': self._chars['color-temperature'],
'value': int(temperature)})
characteristics.append({'aid': self._aid,
'iid': self._chars['on'],
'value': True})
body = json.dumps({'characteristics': characteristics})
self._securecon.put('/characteristics', body)
def turn_off(self, **kwargs):
"""Turn the specified light off."""
characteristics = [{'aid': self._aid,
'iid': self._chars['on'],
'value': False}]
body = json.dumps({'characteristics': characteristics})
self._securecon.put('/characteristics', body)
+13 -20
View File
@@ -49,11 +49,17 @@ GROUP_MIN_API_VERSION = (1, 13, 0)
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Hue lights."""
if discovery_info is None:
return
"""Old way of setting up Hue lights.
bridge = hass.data[hue.DOMAIN][discovery_info['host']]
Can only be called when a user accidentally mentions hue platform in their
config. But even in that case it would have been ignored.
"""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the Hue lights from a config entry."""
bridge = hass.data[hue.DOMAIN][config_entry.data['host']]
cur_lights = {}
cur_groups = {}
@@ -236,26 +242,13 @@ class HueLight(Light):
@property
def hs_color(self):
"""Return the hs color value."""
# pylint: disable=redefined-outer-name
mode = self._color_mode
if mode not in ('hs', 'xy'):
return
source = self.light.action if self.is_group else self.light.state
hue = source.get('hue')
sat = source.get('sat')
if mode in ('xy', 'hs'):
return color.color_xy_to_hs(*source['xy'])
# Sometimes the state will not include valid hue/sat values.
# Reported as issue 13434
if hue is not None and sat is not None:
return hue / 65535 * 360, sat / 255 * 100
if 'xy' not in source:
return None
return color.color_xy_to_hs(*source['xy'])
return None
@property
def color_temp(self):
+1 -1
View File
@@ -79,7 +79,7 @@ class IGloLamp(Light):
@property
def hs_color(self):
"""Return the hs value."""
return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb'])
return color_util.color_RGB_to_hs(*self._lamp.state()['rgb'])
@property
def effect(self):
@@ -1,11 +1,6 @@
"""
Support for Nanoleaf Aurora platform.
Based in large parts upon Software-2's ha-aurora and fully
reliant on Software-2's nanoleaf-aurora Python Library, see
https://github.com/software-2/ha-aurora as well as
https://github.com/software-2/nanoleaf
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.nanoleaf_aurora/
"""
@@ -15,9 +10,9 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR,
SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_COLOR, PLATFORM_SCHEMA, Light)
from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME
PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, Light)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
import homeassistant.helpers.config_validation as cv
from homeassistant.util import color as color_util
from homeassistant.util.color import \
@@ -25,20 +20,24 @@ from homeassistant.util.color import \
REQUIREMENTS = ['nanoleaf==0.4.1']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Aurora'
ICON = 'mdi:triangle-outline'
SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
SUPPORT_COLOR)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): cv.string,
vol.Optional(CONF_NAME, default='Aurora'): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Nanoleaf Aurora device."""
"""Set up the Nanoleaf Aurora device."""
import nanoleaf
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
@@ -47,8 +46,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
aurora_light.hass_name = name
if aurora_light.on is None:
_LOGGER.error("Could not connect to \
Nanoleaf Aurora: %s on %s", name, host)
_LOGGER.error(
"Could not connect to Nanoleaf Aurora: %s on %s", name, host)
return
add_devices([AuroraLight(aurora_light)], True)
@@ -56,7 +57,7 @@ class AuroraLight(Light):
"""Representation of a Nanoleaf Aurora."""
def __init__(self, light):
"""Initialize an Aurora."""
"""Initialize an Aurora light."""
self._brightness = None
self._color_temp = None
self._effect = None
@@ -99,7 +100,7 @@ class AuroraLight(Light):
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return "mdi:triangle-outline"
return ICON
@property
def is_on(self):
@@ -141,10 +142,7 @@ class AuroraLight(Light):
self._light.on = False
def update(self):
"""Fetch new state data for this light.
This is the only method that should fetch new data for Home Assistant.
"""
"""Fetch new state data for this light."""
self._brightness = self._light.brightness
self._color_temp = self._light.color_temperature
self._effect = self._light.effect
+2 -2
View File
@@ -27,9 +27,9 @@ class QSLight(QSToggleEntity, Light):
@property
def brightness(self):
"""Return the brightness of this light (0-255)."""
return self._qsusb[self.qsid, 1] if self._dim else None
return self.device.value if self.device.is_dimmer else None
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS if self._dim else 0
return SUPPORT_BRIGHTNESS if self.device.is_dimmer else 0
+8 -8
View File
@@ -11,8 +11,8 @@ import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_NAME)
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA)
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
@@ -90,15 +90,15 @@ class TPLinkSmartBulb(Light):
if ATTR_COLOR_TEMP in kwargs:
self.smartbulb.color_temp = \
mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
if ATTR_KELVIN in kwargs:
self.smartbulb.color_temp = kwargs[ATTR_KELVIN]
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255)
self.smartbulb.brightness = brightness_to_percentage(brightness)
brightness = brightness_to_percentage(
kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255))
if ATTR_HS_COLOR in kwargs:
hue, sat = kwargs.get(ATTR_HS_COLOR)
hsv = (hue, sat, 100)
hsv = (int(hue), int(sat), brightness)
self.smartbulb.hsv = hsv
elif ATTR_BRIGHTNESS in kwargs:
self.smartbulb.brightness = brightness
def turn_off(self, **kwargs):
"""Turn the light off."""
+12 -1
View File
@@ -24,6 +24,14 @@ REQUIREMENTS = ['yeelight==0.4.0']
_LOGGER = logging.getLogger(__name__)
LEGACY_DEVICE_TYPE_MAP = {
'color1': 'rgb',
'mono1': 'white',
'strip1': 'strip',
'bslamp1': 'bedside',
'ceiling1': 'ceiling',
}
CONF_TRANSITION = 'transition'
DEFAULT_TRANSITION = 350
@@ -122,8 +130,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is not None:
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
device_type = discovery_info['device_type']
device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type)
# Not using hostname, as it seems to vary.
name = "yeelight_%s_%s" % (discovery_info['device_type'],
name = "yeelight_%s_%s" % (device_type,
discovery_info['properties']['mac'])
device = {'name': name, 'ipaddr': discovery_info['host']}
@@ -38,6 +38,7 @@ class BMWLock(LockDevice):
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name
self._state = None
@@ -49,6 +50,11 @@ class BMWLock(LockDevice):
"""
return False
@property
def unique_id(self):
"""Return the unique ID of the lock."""
return self._unique_id
@property
def name(self):
"""Return the name of the lock."""
+1 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pylutron-caseta==0.3.0']
REQUIREMENTS = ['pylutron-caseta==0.5.0']
_LOGGER = logging.getLogger(__name__)
+34 -12
View File
@@ -22,12 +22,22 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 62910
DOMAIN = 'maxcube'
MAXCUBE_HANDLE = 'maxcube'
DATA_KEY = 'maxcube'
NOTIFICATION_ID = 'maxcube_notification'
NOTIFICATION_TITLE = 'Max!Cube gateway setup'
CONF_GATEWAYS = 'gateways'
CONFIG_GATEWAY = vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(CONF_GATEWAYS, default={}):
vol.All(cv.ensure_list, [CONFIG_GATEWAY])
}),
}, extra=vol.ALLOW_EXTRA)
@@ -36,19 +46,31 @@ def setup(hass, config):
"""Establish connection to MAX! Cube."""
from maxcube.connection import MaxCubeConnection
from maxcube.cube import MaxCube
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
host = config.get(DOMAIN).get(CONF_HOST)
port = config.get(DOMAIN).get(CONF_PORT)
connection_failed = 0
gateways = config[DOMAIN][CONF_GATEWAYS]
for gateway in gateways:
host = gateway[CONF_HOST]
port = gateway[CONF_PORT]
try:
cube = MaxCube(MaxCubeConnection(host, port))
except timeout:
_LOGGER.error("Connection to Max!Cube could not be established")
cube = None
try:
cube = MaxCube(MaxCubeConnection(host, port))
hass.data[DATA_KEY][host] = MaxCubeHandle(cube)
except timeout as ex:
_LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart Home Assistant after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
connection_failed += 1
if connection_failed >= len(gateways):
return False
hass.data[MAXCUBE_HANDLE] = MaxCubeHandle(cube)
load_platform(hass, 'climate', DOMAIN)
load_platform(hass, 'binary_sensor', DOMAIN)
+1 -1
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.04.03']
REQUIREMENTS = ['youtube_dl==2018.04.16']
_LOGGER = logging.getLogger(__name__)
@@ -0,0 +1,213 @@
"""
Support for interfacing with Monoprice Blackbird 4k 8x8 HDBaseT Matrix.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.blackbird
"""
import logging
import voluptuous as vol
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)
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
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
SOURCE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
CONF_ZONES = 'zones'
CONF_SOURCES = 'sources'
CONF_TYPE = 'type'
DATA_BLACKBIRD = 'blackbird'
SERVICE_SETALLZONES = 'blackbird_set_all_zones'
ATTR_SOURCE = 'source'
BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_SOURCE): cv.string
})
# Valid zone ids: 1-8
ZONE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8))
# Valid source ids: 1-8
SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8))
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TYPE): vol.In(['serial', 'socket']),
vol.Optional(CONF_PORT): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}),
vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}),
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform."""
port = config.get(CONF_PORT)
host = config.get(CONF_HOST)
device_type = config.get(CONF_TYPE)
import socket
from pyblackbird import get_blackbird
from serial import SerialException
if device_type == 'serial':
if port is None:
_LOGGER.error("No port configured")
return
try:
blackbird = get_blackbird(port)
except SerialException:
_LOGGER.error("Error connecting to the Blackbird controller")
return
elif device_type == 'socket':
try:
if host is None:
_LOGGER.error("No host configured")
return
blackbird = get_blackbird(host, False)
except socket.timeout:
_LOGGER.error("Error connecting to the Blackbird controller")
return
else:
_LOGGER.error("Incorrect device type specified")
return
sources = {source_id: extra[CONF_NAME] for source_id, extra
in config[CONF_SOURCES].items()}
hass.data[DATA_BLACKBIRD] = []
for zone_id, extra in config[CONF_ZONES].items():
_LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME])
hass.data[DATA_BLACKBIRD].append(BlackbirdZone(
blackbird, sources, zone_id, extra[CONF_NAME]))
add_devices(hass.data[DATA_BLACKBIRD], True)
def service_handle(service):
"""Handle for services."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
source = service.data.get(ATTR_SOURCE)
if entity_ids:
devices = [device for device in hass.data[DATA_BLACKBIRD]
if device.entity_id in entity_ids]
else:
devices = hass.data[DATA_BLACKBIRD]
for device in devices:
if service.service == SERVICE_SETALLZONES:
device.set_all_zones(source)
hass.services.register(DOMAIN, SERVICE_SETALLZONES, service_handle,
schema=BLACKBIRD_SETALLZONES_SCHEMA)
class BlackbirdZone(MediaPlayerDevice):
"""Representation of a Blackbird matrix zone."""
def __init__(self, blackbird, sources, zone_id, zone_name):
"""Initialize new zone."""
self._blackbird = blackbird
# dict source_id -> source name
self._source_id_name = sources
# dict source name -> source_id
self._source_name_id = {v: k for k, v in sources.items()}
# ordered list of all source names
self._source_names = sorted(self._source_name_id.keys(),
key=lambda v: self._source_name_id[v])
self._zone_id = zone_id
self._name = zone_name
self._state = None
self._source = None
def update(self):
"""Retrieve latest state."""
state = self._blackbird.zone_status(self._zone_id)
if not state:
return False
self._state = STATE_ON if state.power else STATE_OFF
idx = state.av
if idx in self._source_id_name:
self._source = self._source_id_name[idx]
else:
self._source = None
return True
@property
def name(self):
"""Return the name of the zone."""
return self._name
@property
def state(self):
"""Return the state of the zone."""
return self._state
@property
def supported_features(self):
"""Return flag of media commands that are supported."""
return SUPPORT_BLACKBIRD
@property
def media_title(self):
"""Return the current source as media title."""
return self._source
@property
def source(self):
"""Return the current input source of the device."""
return self._source
@property
def source_list(self):
"""List of available input sources."""
return self._source_names
def set_all_zones(self, source):
"""Set all zones to one source."""
_LOGGER.debug("Setting all zones")
if source not in self._source_name_id:
return
idx = self._source_name_id[source]
_LOGGER.debug("Setting all zones source to %s", idx)
self._blackbird.set_all_zone_source(idx)
def select_source(self, source):
"""Set input source."""
if source not in self._source_name_id:
return
idx = self._source_name_id[source]
_LOGGER.debug("Setting zone %d source to %s", self._zone_id, idx)
self._blackbird.set_zone_source(self._zone_id, idx)
def turn_on(self):
"""Turn the media player on."""
_LOGGER.debug("Turning zone %d on", self._zone_id)
self._blackbird.set_zone_power(self._zone_id, True)
def turn_off(self):
"""Turn the media player off."""
_LOGGER.debug("Turning zone %d off", self._zone_id)
self._blackbird.set_zone_power(self._zone_id, False)
@@ -37,30 +37,30 @@ REQUIREMENTS = ['xmltodict==0.11.0']
_LOGGER = logging.getLogger(__name__)
STATE_GROUPED = 'grouped'
ATTR_MASTER = 'master'
SERVICE_JOIN = 'bluesound_join'
SERVICE_UNJOIN = 'bluesound_unjoin'
SERVICE_SET_TIMER = 'bluesound_set_sleep_timer'
SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer'
DATA_BLUESOUND = 'bluesound'
DEFAULT_PORT = 11000
SYNC_STATUS_INTERVAL = timedelta(minutes=5)
UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30)
UPDATE_SERVICES_INTERVAL = timedelta(minutes=30)
UPDATE_PRESETS_INTERVAL = timedelta(minutes=30)
NODE_OFFLINE_CHECK_TIMEOUT = 180
NODE_RETRY_INITIATION = timedelta(minutes=3)
SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer'
SERVICE_JOIN = 'bluesound_join'
SERVICE_SET_TIMER = 'bluesound_set_sleep_timer'
SERVICE_UNJOIN = 'bluesound_unjoin'
STATE_GROUPED = 'grouped'
SYNC_STATUS_INTERVAL = timedelta(minutes=5)
UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30)
UPDATE_PRESETS_INTERVAL = timedelta(minutes=30)
UPDATE_SERVICES_INTERVAL = timedelta(minutes=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}])
})
@@ -131,8 +131,8 @@ def _add_player(hass, async_add_devices, host, port=None, name=None):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player)
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the Bluesound platforms."""
if DATA_BLUESOUND not in hass.data:
hass.data[DATA_BLUESOUND] = []
@@ -202,6 +202,9 @@ class BluesoundPlayer(MediaPlayerDevice):
if self.port is None:
self.port = DEFAULT_PORT
class _TimeoutException(Exception):
pass
@staticmethod
def _try_get_index(string, search_string):
"""Get the index."""
@@ -258,7 +261,8 @@ class BluesoundPlayer(MediaPlayerDevice):
while True:
await self.async_update_status()
except (asyncio.TimeoutError, ClientError):
except (asyncio.TimeoutError, ClientError,
BluesoundPlayer._TimeoutException):
_LOGGER.info("Node %s is offline, retrying later", self._name)
await asyncio.sleep(
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
@@ -293,8 +297,8 @@ class BluesoundPlayer(MediaPlayerDevice):
self._retry_remove = async_track_time_interval(
self._hass, self.async_init, NODE_RETRY_INITIATION)
except Exception:
_LOGGER.exception("Unexpected when initiating error in %s",
self.host)
_LOGGER.exception(
"Unexpected when initiating error in %s", self.host)
raise
async def async_update(self):
@@ -307,8 +311,8 @@ class BluesoundPlayer(MediaPlayerDevice):
await self.async_update_captures()
await self.async_update_services()
async def send_bluesound_command(self, method, raise_timeout=False,
allow_offline=False):
async def send_bluesound_command(
self, method, raise_timeout=False, allow_offline=False):
"""Send command to the player."""
import xmltodict
@@ -321,6 +325,7 @@ class BluesoundPlayer(MediaPlayerDevice):
_LOGGER.debug("Calling URL: %s", url)
response = None
try:
websession = async_get_clientsession(self._hass)
with async_timeout.timeout(10, loop=self._hass.loop):
@@ -332,6 +337,9 @@ class BluesoundPlayer(MediaPlayerDevice):
data = None
else:
data = xmltodict.parse(result)
elif response.status == 595:
_LOGGER.info("Status 595 returned, treating as timeout")
raise BluesoundPlayer._TimeoutException()
else:
_LOGGER.error("Error %s on %s", response.status, url)
return None
@@ -366,13 +374,9 @@ class BluesoundPlayer(MediaPlayerDevice):
with async_timeout.timeout(125, loop=self._hass.loop):
response = await self._polling_session.get(
url,
headers={CONNECTION: KEEP_ALIVE})
url, headers={CONNECTION: KEEP_ALIVE})
if response.status != 200:
_LOGGER.error("Error %s on %s. Trying one more time.",
response.status, url)
else:
if response.status == 200:
result = await response.text()
self._is_online = True
self._last_status_update = dt_util.utcnow()
@@ -380,8 +384,8 @@ class BluesoundPlayer(MediaPlayerDevice):
group_name = self._status.get('groupName', None)
if group_name != self._group_name:
_LOGGER.debug('Group name change detected on device: %s',
self.host)
_LOGGER.debug(
"Group name change detected on device: %s", self.host)
self._group_name = group_name
# the sleep is needed to make sure that the
# devices is synced
@@ -398,14 +402,20 @@ class BluesoundPlayer(MediaPlayerDevice):
await self.force_update_sync_status()
self.async_schedule_update_ha_state()
elif response.status == 595:
_LOGGER.info("Status 595 returned, treating as timeout")
raise BluesoundPlayer._TimeoutException()
else:
_LOGGER.error("Error %s on %s. Trying one more time",
response.status, url)
except (asyncio.TimeoutError, ClientError):
self._is_online = False
self._last_status_update = None
self._status = None
self.async_schedule_update_ha_state()
_LOGGER.info("Client connection error, marking %s as offline",
self._name)
_LOGGER.info(
"Client connection error, marking %s as offline", self._name)
raise
async def async_trigger_sync_on_all(self):
@@ -416,8 +426,8 @@ class BluesoundPlayer(MediaPlayerDevice):
await player.force_update_sync_status()
@Throttle(SYNC_STATUS_INTERVAL)
async def async_update_sync_status(self, on_updated_cb=None,
raise_timeout=False):
async def async_update_sync_status(
self, on_updated_cb=None, raise_timeout=False):
"""Update sync status."""
await self.force_update_sync_status(
on_updated_cb, raise_timeout=False)
@@ -465,7 +475,7 @@ class BluesoundPlayer(MediaPlayerDevice):
'image': item.get('@image', ''),
'is_raw_url': True,
'url2': item.get('@url', ''),
'url': 'Preset?id=' + item.get('@id', '')
'url': 'Preset?id={}'.format(item.get('@id', ''))
})
if 'presets' in resp and 'preset' in resp['presets']:
@@ -503,11 +513,6 @@ class BluesoundPlayer(MediaPlayerDevice):
return self._services_items
@property
def should_poll(self):
"""No need to poll information."""
return True
@property
def media_content_type(self):
"""Content type of current playing media."""
@@ -803,22 +808,22 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_add_slave(self, slave_device):
"""Add slave to master."""
return self.send_bluesound_command('/AddSlave?slave={}&port={}'
.format(slave_device.host,
slave_device.port))
return await self.send_bluesound_command(
'/AddSlave?slave={}&port={}'.format(
slave_device.host, slave_device.port))
async def async_remove_slave(self, slave_device):
"""Remove slave to master."""
return self.send_bluesound_command('/RemoveSlave?slave={}&port={}'
.format(slave_device.host,
slave_device.port))
return await self.send_bluesound_command(
'/RemoveSlave?slave={}&port={}'.format(
slave_device.host, slave_device.port))
async def async_increase_timer(self):
"""Increase sleep time on player."""
sleep_time = await self.send_bluesound_command('/Sleep')
if sleep_time is None:
_LOGGER.error('Error while increasing sleep time on player: %s',
self.host)
_LOGGER.error(
"Error while increasing sleep time on player: %s", self.host)
return 0
return int(sleep_time.get('sleep', '0'))
@@ -831,8 +836,9 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_set_shuffle(self, shuffle):
"""Enable or disable shuffle mode."""
return self.send_bluesound_command('/Shuffle?state={}'
.format('1' if shuffle else '0'))
value = '1' if shuffle else '0'
return await self.send_bluesound_command(
'/Shuffle?state={}'.format(value))
async def async_select_source(self, source):
"""Select input source."""
@@ -856,14 +862,14 @@ class BluesoundPlayer(MediaPlayerDevice):
if 'is_raw_url' in selected_source and selected_source['is_raw_url']:
url = selected_source['url']
return self.send_bluesound_command(url)
return await self.send_bluesound_command(url)
async def async_clear_playlist(self):
"""Clear players playlist."""
if self.is_grouped and not self.is_master:
return
return self.send_bluesound_command('Clear')
return await self.send_bluesound_command('Clear')
async def async_media_next_track(self):
"""Send media_next command to media player."""
@@ -877,7 +883,7 @@ class BluesoundPlayer(MediaPlayerDevice):
action['@name'] == 'skip'):
cmd = action['@url']
return self.send_bluesound_command(cmd)
return await self.send_bluesound_command(cmd)
async def async_media_previous_track(self):
"""Send media_previous command to media player."""
@@ -891,35 +897,36 @@ class BluesoundPlayer(MediaPlayerDevice):
action['@name'] == 'back'):
cmd = action['@url']
return self.send_bluesound_command(cmd)
return await self.send_bluesound_command(cmd)
async def async_media_play(self):
"""Send media_play command to media player."""
if self.is_grouped and not self.is_master:
return
return self.send_bluesound_command('Play')
return await self.send_bluesound_command('Play')
async def async_media_pause(self):
"""Send media_pause command to media player."""
if self.is_grouped and not self.is_master:
return
return self.send_bluesound_command('Pause')
return await self.send_bluesound_command('Pause')
async def async_media_stop(self):
"""Send stop command."""
if self.is_grouped and not self.is_master:
return
return self.send_bluesound_command('Pause')
return await self.send_bluesound_command('Pause')
async def async_media_seek(self, position):
"""Send media_seek command to media player."""
if self.is_grouped and not self.is_master:
return
return self.send_bluesound_command('Play?seek=' + str(float(position)))
return await self.send_bluesound_command(
'Play?seek={}'.format(float(position)))
async def async_play_media(self, media_type, media_id, **kwargs):
"""
@@ -933,9 +940,9 @@ class BluesoundPlayer(MediaPlayerDevice):
url = 'Play?url={}'.format(media_id)
if kwargs.get(ATTR_MEDIA_ENQUEUE):
return self.send_bluesound_command(url)
return await self.send_bluesound_command(url)
return self.send_bluesound_command(url)
return await self.send_bluesound_command(url)
async def async_volume_up(self):
"""Volume up the media player."""
@@ -957,7 +964,7 @@ class BluesoundPlayer(MediaPlayerDevice):
volume = 0
elif volume > 1:
volume = 1
return self.send_bluesound_command(
return await self.send_bluesound_command(
'Volume?level=' + str(float(volume) * 100))
async def async_mute_volume(self, mute):
@@ -966,7 +973,7 @@ class BluesoundPlayer(MediaPlayerDevice):
volume = self.volume_level
if volume > 0:
self._lastvol = volume
return self.send_bluesound_command('Volume?level=0')
return await self.send_bluesound_command('Volume?level=0')
else:
return self.send_bluesound_command(
return await self.send_bluesound_command(
'Volume?level=' + str(float(self._lastvol) * 100))
+22 -28
View File
@@ -306,13 +306,18 @@ class CastDevice(MediaPlayerDevice):
_LOGGER.debug("Discovered chromecast with same UUID: %s", discover)
self.hass.async_add_job(self.async_set_cast_info(discover))
async def async_stop(event):
"""Disconnect socket on Home Assistant stop."""
await self._async_disconnect()
async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED,
async_cast_discovered)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop)
self.hass.async_add_job(self.async_set_cast_info(self._cast_info))
async def async_will_remove_from_hass(self) -> None:
"""Disconnect Chromecast object when removed."""
self._async_disconnect()
await self._async_disconnect()
if self._cast_info.uuid is not None:
# Remove the entity from the added casts so that it can dynamically
# be re-added again.
@@ -328,7 +333,7 @@ class CastDevice(MediaPlayerDevice):
if old_cast_info.host_port == cast_info.host_port:
# Nothing connection-related updated
return
self._async_disconnect()
await self._async_disconnect()
# Failed connection will unfortunately never raise an exception, it
# will instead just try connecting indefinitely.
@@ -348,37 +353,27 @@ class CastDevice(MediaPlayerDevice):
_LOGGER.debug("Connection successful!")
self.async_schedule_update_ha_state()
@callback
def _async_disconnect(self):
async def _async_disconnect(self):
"""Disconnect Chromecast object if it is set."""
if self._chromecast is None:
# Can't disconnect if not connected.
return
_LOGGER.debug("Disconnecting from previous chromecast socket.")
_LOGGER.debug("Disconnecting from chromecast socket.")
self._available = False
self._chromecast.disconnect(blocking=False)
self.async_schedule_update_ha_state()
await self.hass.async_add_job(self._chromecast.disconnect)
# Invalidate some attributes
self._chromecast = None
self.cast_status = None
self.media_status = None
self.media_status_received = None
self._status_listener.invalidate()
self._status_listener = None
if self._status_listener is not None:
self._status_listener.invalidate()
self._status_listener = None
def update(self):
"""Periodically update the properties.
Even though we receive callbacks for most state changes, some 3rd party
apps don't always send them. Better poll every now and then if the
chromecast is active (i.e. an app is running).
"""
if not self._available:
# Not connected or not available.
return
if self._chromecast.media_controller.is_active:
# We can only update status if the media namespace is active
self._chromecast.media_controller.update_status()
self.async_schedule_update_ha_state()
# ========== Callbacks ==========
def new_cast_status(self, cast_status):
@@ -466,8 +461,8 @@ class CastDevice(MediaPlayerDevice):
# ========== Properties ==========
@property
def should_poll(self):
"""Polling needed for cast integration, see async_update."""
return True
"""No polling needed."""
return False
@property
def name(self):
@@ -596,11 +591,10 @@ class CastDevice(MediaPlayerDevice):
def media_position(self):
"""Position of current playing media in seconds."""
if self.media_status is None or \
not (self.media_status.player_is_playing or
self.media_status.player_is_paused or
self.media_status.player_is_idle):
not (self.media_status.player_is_playing or
self.media_status.player_is_paused or
self.media_status.player_is_idle):
return None
return self.media_status.current_time
@property
+28 -10
View File
@@ -8,6 +8,7 @@ import asyncio
from collections import OrderedDict
from functools import wraps
import logging
import socket
import urllib
import re
@@ -157,13 +158,29 @@ def _check_deprecated_turn_off(hass, turn_off_action):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Kodi platform."""
if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = []
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT)
encryption = config.get(CONF_PROXY_SSL)
websocket = config.get(CONF_ENABLE_WEBSOCKET)
hass.data[DATA_KODI] = dict()
# Is this a manual configuration?
if discovery_info is None:
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT)
encryption = config.get(CONF_PROXY_SSL)
websocket = config.get(CONF_ENABLE_WEBSOCKET)
else:
name = "{} ({})".format(DEFAULT_NAME, discovery_info.get('hostname'))
host = discovery_info.get('host')
port = discovery_info.get('port')
tcp_port = DEFAULT_TCP_PORT
encryption = DEFAULT_PROXY_SSL
websocket = DEFAULT_ENABLE_WEBSOCKET
# Only add a device once, so discovered devices do not override manual
# config.
ip_addr = socket.gethostbyname(host)
if ip_addr in hass.data[DATA_KODI]:
return
entity = KodiDevice(
hass,
@@ -175,7 +192,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
turn_off_action=config.get(CONF_TURN_OFF_ACTION),
timeout=config.get(CONF_TIMEOUT), websocket=websocket)
hass.data[DATA_KODI].append(entity)
hass.data[DATA_KODI][ip_addr] = entity
async_add_devices([entity], update_before_add=True)
@asyncio.coroutine
@@ -189,10 +206,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if key != 'entity_id'}
entity_ids = service.data.get('entity_id')
if entity_ids:
target_players = [player for player in hass.data[DATA_KODI]
target_players = [player
for player in hass.data[DATA_KODI].values()
if player.entity_id in entity_ids]
else:
target_players = hass.data[DATA_KODI]
target_players = hass.data[DATA_KODI].values()
update_tasks = []
for player in target_players:
@@ -8,6 +8,7 @@ import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
@@ -20,11 +21,11 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF,
CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY,
STATE_UNAVAILABLE
STATE_UNAVAILABLE, EVENT_HOMEASSISTANT_STOP
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymediaroom==0.6']
REQUIREMENTS = ['pymediaroom==0.6.3']
_LOGGER = logging.getLogger(__name__)
@@ -81,12 +82,21 @@ async def async_setup_platform(hass, config, async_add_devices,
if not config[CONF_OPTIMISTIC]:
from pymediaroom import install_mediaroom_protocol
already_installed = hass.data.get(DISCOVERY_MEDIAROOM, False)
already_installed = hass.data.get(DISCOVERY_MEDIAROOM, None)
if not already_installed:
await install_mediaroom_protocol(
hass.data[DISCOVERY_MEDIAROOM] = await install_mediaroom_protocol(
responses_callback=callback_notify)
@callback
def stop_discovery(event):
"""Stop discovery of new mediaroom STB's."""
_LOGGER.debug("Stopping internal pymediaroom discovery.")
hass.data[DISCOVERY_MEDIAROOM].close()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
stop_discovery)
_LOGGER.debug("Auto discovery installed")
hass.data[DISCOVERY_MEDIAROOM] = True
class MediaroomDevice(MediaPlayerDevice):
@@ -120,7 +130,7 @@ class MediaroomDevice(MediaPlayerDevice):
self._channel = None
self._optimistic = optimistic
self._state = STATE_PLAYING if optimistic else STATE_STANDBY
self._name = 'Mediaroom {}'.format(device_id)
self._name = 'Mediaroom {}'.format(device_id if device_id else host)
self._available = True
if device_id:
self._unique_id = device_id
+1 -1
View File
@@ -23,7 +23,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['python-mpd2==0.5.5']
REQUIREMENTS = ['python-mpd2==1.0.0']
_LOGGER = logging.getLogger(__name__)
+82 -4
View File
@@ -22,6 +22,7 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4']
_LOGGER = logging.getLogger(__name__)
CONF_SOURCES = 'sources'
CONF_ZONE2 = 'zone2'
DEFAULT_NAME = 'Onkyo Receiver'
@@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES):
{cv.string: cv.string},
vol.Optional(CONF_ZONE2, default=False): cv.boolean,
})
@@ -57,6 +59,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
eiscp.eISCP(host), config.get(CONF_SOURCES),
name=config.get(CONF_NAME)))
KNOWN_HOSTS.append(host)
# Add Zone2 if configured
if config.get(CONF_ZONE2):
_LOGGER.debug("Setting up zone 2")
hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host),
config.get(CONF_SOURCES),
name=config.get(CONF_NAME) +
" Zone 2"))
except OSError:
_LOGGER.error("Unable to connect to receiver at %s", host)
else:
@@ -98,8 +108,9 @@ class OnkyoDevice(MediaPlayerDevice):
return result
def update(self):
"""Get the latest details from the device."""
"""Get the latest state from the device."""
status = self.command('system-power query')
if not status:
return
if status[1] == 'on':
@@ -107,9 +118,11 @@ class OnkyoDevice(MediaPlayerDevice):
else:
self._pwstate = STATE_OFF
return
volume_raw = self.command('volume query')
mute_raw = self.command('audio-muting query')
current_source_raw = self.command('input-selector query')
if not (volume_raw and mute_raw and current_source_raw):
return
@@ -147,12 +160,12 @@ class OnkyoDevice(MediaPlayerDevice):
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
"""Return boolean indicating mute status."""
return self._muted
@property
def supported_features(self):
"""Flag media player features that are supported."""
"""Return media player features that are supported."""
return SUPPORT_ONKYO
@property
@@ -166,7 +179,7 @@ class OnkyoDevice(MediaPlayerDevice):
return self._source_list
def turn_off(self):
"""Turn off media player."""
"""Turn the media player off."""
self.command('system-power standby')
def set_volume_level(self, volume):
@@ -189,3 +202,68 @@ class OnkyoDevice(MediaPlayerDevice):
if source in self._source_list:
source = self._reverse_mapping[source]
self.command('input-selector {}'.format(source))
class OnkyoDeviceZone2(OnkyoDevice):
"""Representation of an Onkyo device's zone 2."""
def update(self):
"""Get the latest state from the device."""
status = self.command('zone2.power=query')
if not status:
return
if status[1] == 'on':
self._pwstate = STATE_ON
else:
self._pwstate = STATE_OFF
return
volume_raw = self.command('zone2.volume=query')
mute_raw = self.command('zone2.muting=query')
current_source_raw = self.command('zone2.selector=query')
if not (volume_raw and mute_raw and current_source_raw):
return
# eiscp can return string or tuple. Make everything tuples.
if isinstance(current_source_raw[1], str):
current_source_tuples = \
(current_source_raw[0], (current_source_raw[1],))
else:
current_source_tuples = current_source_raw
for source in current_source_tuples[1]:
if source in self._source_mapping:
self._current_source = self._source_mapping[source]
break
else:
self._current_source = '_'.join(
[i for i in current_source_tuples[1]])
self._muted = bool(mute_raw[1] == 'on')
self._volume = volume_raw[1] / 80.0
def turn_off(self):
"""Turn the media player off."""
self.command('zone2.power=standby')
def set_volume_level(self, volume):
"""Set volume level, input is range 0..1. Onkyo ranges from 1-80."""
self.command('zone2.volume={}'.format(int(volume*80)))
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
if mute:
self.command('zone2.muting=on')
else:
self.command('zone2.muting=off')
def turn_on(self):
"""Turn the media player on."""
self.command('zone2.power=on')
def select_source(self, source):
"""Set the input source."""
if source in self._source_list:
source = self._reverse_mapping[source]
self.command('zone2.selector={}'.format(source))
@@ -402,3 +402,13 @@ songpal_set_sound_setting:
value:
description: Value to set.
example: 'on'
blackbird_set_all_zones:
description: Set all Blackbird zones to a single source.
fields:
entity_id:
description: Name of any blackbird zone.
example: 'media_player.zone_1'
source:
description: Name of source to switch to.
example: 'Source 1'
@@ -266,6 +266,8 @@ class SqueezeBoxDevice(MediaPlayerDevice):
if response is False:
return
last_media_position = self.media_position
self._status = {}
try:
@@ -278,7 +280,11 @@ class SqueezeBoxDevice(MediaPlayerDevice):
pass
self._status.update(response)
self._last_update = utcnow()
if self.media_position != last_media_position:
_LOGGER.debug('Media position updated for %s: %s',
self, self.media_position)
self._last_update = utcnow()
@property
def volume_level(self):
@@ -35,6 +35,7 @@ CONF_SOURCES = 'sources'
CONF_ON_ACTION = 'turn_on_action'
DEFAULT_NAME = 'LG webOS Smart TV'
LIVETV_APP_ID = 'com.webos.app.livetv'
WEBOSTV_CONFIG_FILE = 'webostv.conf'
@@ -343,6 +344,42 @@ class LgWebOSDevice(MediaPlayerDevice):
self._current_source = source_dict['label']
self._client.set_input(source_dict['id'])
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
_LOGGER.debug(
"Call play media type <%s>, Id <%s>", media_type, media_id)
if media_type == MEDIA_TYPE_CHANNEL:
_LOGGER.debug("Searching channel...")
partial_match_channel_id = None
for channel in self._client.get_channels():
_LOGGER.debug(
"Checking channel number <%s>, name <%s>, id <%s>...",
channel['channelNumber'],
channel['channelName'],
channel['channelId'])
if media_id == channel['channelNumber']:
_LOGGER.debug(
"Perfect match on channel number: switching!")
self._client.set_channel(channel['channelId'])
return
elif media_id.lower() == channel['channelName'].lower():
_LOGGER.debug(
"Perfect match on channel name: switching!")
self._client.set_channel(channel['channelId'])
return
elif media_id.lower() in channel['channelName'].lower():
_LOGGER.debug(
"Partial match on channel name: saving it...")
partial_match_channel_id = channel['channelId']
if partial_match_channel_id is not None:
_LOGGER.debug(
"Using partial match on channel name: switching!")
self._client.set_channel(partial_match_channel_id)
return
def media_play(self):
"""Send play command."""
self._playing = True
@@ -357,8 +394,16 @@ class LgWebOSDevice(MediaPlayerDevice):
def media_next_track(self):
"""Send next track command."""
self._client.channel_up()
current_input = self._client.get_input()
if current_input == LIVETV_APP_ID:
self._client.channel_up()
else:
self._client.fast_forward()
def media_previous_track(self):
"""Send the previous track command."""
self._client.channel_down()
current_input = self._client.get_input()
if current_input == LIVETV_APP_ID:
self._client.channel_down()
else:
self._client.rewind()
+11 -12
View File
@@ -37,7 +37,8 @@ PLATFORM_SCHEMA = vol.Schema(
vol.All(PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_RECIPIENT): cv.string,
vol.Required(CONF_RECIPIENT, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_SENDER): cv.string,
}), validate_sender))
@@ -59,21 +60,19 @@ class ClicksendNotificationService(BaseNotificationService):
"""Initialize the service."""
self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY)
self.recipient = config.get(CONF_RECIPIENT)
self.recipients = config.get(CONF_RECIPIENT)
self.sender = config.get(CONF_SENDER, CONF_RECIPIENT)
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
data = ({
'messages': [
{
'source': 'hass.notify',
'from': self.sender,
'to': self.recipient,
'body': message,
}
]
})
data = {"messages": []}
for recipient in self.recipients:
data["messages"].append({
'source': 'hass.notify',
'from': self.sender,
'to': recipient,
'body': message,
})
api_url = "{}/sms/send".format(BASE_API_URL)
+54 -18
View File
@@ -4,6 +4,7 @@ Facebook platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.facebook/
"""
import json
import logging
from aiohttp.hdrs import CONTENT_TYPE
@@ -19,6 +20,8 @@ _LOGGER = logging.getLogger(__name__)
CONF_PAGE_ACCESS_TOKEN = 'page_access_token'
BASE_URL = 'https://graph.facebook.com/v2.6/me/messages'
CREATE_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/message_creatives'
SEND_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/broadcast_messages'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string,
@@ -55,27 +58,60 @@ class FacebookNotificationService(BaseNotificationService):
_LOGGER.error("At least 1 target is required")
return
for target in targets:
# If the target starts with a "+", we suppose it's a phone number,
# otherwise it's a user id.
if target.startswith('+'):
recipient = {"phone_number": target}
else:
recipient = {"id": target}
# broadcast message
if targets[0].lower() == 'broadcast':
broadcast_create_body = {"messages": [body_message]}
_LOGGER.debug("Broadcast body %s : ", broadcast_create_body)
body = {
"recipient": recipient,
"message": body_message
resp = requests.post(CREATE_BROADCAST_URL,
data=json.dumps(broadcast_create_body),
params=payload,
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
timeout=10)
_LOGGER.debug("FB Messager broadcast id %s : ", resp.json())
# at this point we get broadcast id
broadcast_body = {
"message_creative_id": resp.json().get('message_creative_id'),
"notification_type": "REGULAR",
}
import json
resp = requests.post(BASE_URL, data=json.dumps(body),
resp = requests.post(SEND_BROADCAST_URL,
data=json.dumps(broadcast_body),
params=payload,
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
timeout=10)
if resp.status_code != 200:
obj = resp.json()
error_message = obj['error']['message']
error_code = obj['error']['code']
_LOGGER.error(
"Error %s : %s (Code %s)", resp.status_code, error_message,
error_code)
log_error(resp)
# non-broadcast message
else:
for target in targets:
# If the target starts with a "+", it's a phone number,
# otherwise it's a user id.
if target.startswith('+'):
recipient = {"phone_number": target}
else:
recipient = {"id": target}
body = {
"recipient": recipient,
"message": body_message
}
resp = requests.post(BASE_URL, data=json.dumps(body),
params=payload,
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
timeout=10)
if resp.status_code != 200:
log_error(resp)
def log_error(response):
"""Log error message."""
obj = response.json()
error_message = obj['error']['message']
error_code = obj['error']['code']
_LOGGER.error(
"Error %s : %s (Code %s)", response.status_code, error_message,
error_code)

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