Compare commits

...

387 Commits

Author SHA1 Message Date
Paulus Schoutsen
9ae6a3402c Version bump to 0.67.0b0 2018-04-06 10:26:08 -04:00
David Broadfoot
bd51143ac1 Added gogogate2 cover (#13467)
* Added gogogate2 cover

* Hound fixes

* PR feedback

* Hound comments

* Bump gogogate2 version

* Update requirements all

* Add device_class and features

* Fix lint issues

* Again lint

* Fix imports

* Fix end of file
2018-04-06 15:53:00 +02:00
Marco Orovecchia
0a25d30ba6 Add support for Nanoleaf Aurora Light Panels (#13456)
* Added support for Nanoleaf Aurora Light Panels

* aurora light module - fixed lint errors

* aurora light module - use SUPPORT_COLOR instead of SUPPORT_RGB_COLOR

* nanoleaf aurora light - support_hs_color instead of rgb

* review comments from armills implemented

* nanoleaf aurora lights - put attributes into constructor (pylint)
2018-04-06 09:34:56 -04:00
Philipp Schmitt
bddfe24753 Fix #10175 (#13713) 2018-04-06 11:21:04 +02:00
Timmo
1d7ecc22f9 Added ENTITY_ID_FORMAT import and set entity_id in __init__ (#13642) 2018-04-06 10:59:09 +02:00
jmtatsch
703eea0c93 Enable autodiscovery for mqtt cameras (#13697)
* Enable autodiscovery for mqtt cameras, BREAKING CHANGE: homogenisation topic->state_topic

* fix line too long

* fix topic->state_topic in test

* image shall not be the state of entity
2018-04-06 00:11:38 -04:00
Mister Wil
b70b23ef83 Update AbodePy version to 0.12.3 (#13709) 2018-04-06 00:10:07 -04:00
Adam Mills
1a9727c75a Send XY color for non-osram hue bulbs (#13665)
* Send XY color for Philips hue bulbs

* Review fixes

* Comment tweaks
2018-04-05 20:17:18 -04:00
ikucuze
e6006e9beb Tahoma switches (#13636)
* Added the ability to switch Tahoma Garage door relay.

Those are special switches that can only be pushed.
Their state is always OFF, they react to the turn_on action, perform it, but stay OFF

* fixing indents and so on

* CI fix
2018-04-05 18:56:09 +02:00
PlanetJ
4008bf5611 Adding configration to disable ip address as a requirement Fixes: #13399 (#13692)
* Adding configration to disable ip address as a requirement Fixes: #13399

* Remove whitespace
2018-04-05 18:45:09 +02:00
tadly
edcb242b6d Add media type separation for video/movie (#13612)
* added media type separation for video/movie

* updated all media_player components to reflect new media type
2018-04-05 18:44:38 +02:00
Robin
0081764ddc Remove unused CONF_WATCHERS (#13678)
`CONF_WATCHERS` was from an earlier version, now unused
2018-04-05 18:07:42 +02:00
Niklas Morberg
bb5484edac Support color temperature in Homekit (#13658)
* Add support for color temperature
* Add test for color temp
2018-04-05 18:06:23 +02:00
shuaiger
63820a78d9 Fix asuswrt ap mode failure (#13693)
* fix asuswrt ap mode failure

When using ap mode, asuswrt device_tracker does dont work properly as ip can not be retrieved from wl command. This version fixed the issue.

* save 1 line code

* another 2 lines saved

* typo correction
2018-04-05 18:00:40 +02:00
Tom Harris
61a3b4ffdb Bump insteonplm to 0.8.6 to fix sensor message handling (#13691) 2018-04-05 17:59:32 +02:00
Johann Kellerman
1b3c3494e8 Coverage & Codeowners (#13700) 2018-04-05 17:58:55 +02:00
Daniel Perna
b2d37f5257 Update ha-philips_js to 0.0.3 (#13702)
* Update requirements_all.txt

* Update philips_js.py
2018-04-05 17:54:17 +02:00
cdce8p
206e38a2ab Update HAP-python to 1.1.8 (#13563)
* Bump version to HAP-python==1.1.8
* Required changes for version change
* Small bugfix lights
2018-04-05 13:20:20 +02:00
John Arild Berentsen
fe56844a3a Bugfix: Zwave Print_node to logfile instead of console (#13302)
* Print to logfile instead of console

* Review changes

* Typo
2018-04-05 11:14:15 +02:00
cdce8p
692b2644c7 Minor style changes, cleanup (#13654)
* Minor style changes, cleanup
* Change 'self._entity.id' to 'self.entity_id'
* Use const 'STATE_OFF'
* Added CATEGORY constants
* Removed *args from accessory types
* Changed 'self._hass' to 'self.hass'
* Added log debug msg (for added lights)
2018-04-05 00:52:25 +02:00
cdce8p
f263a931f7 Bugfixes HomeKit covers, lights (#13689)
* covers -> current_position attribute
* lights -> hue and saturation attribute
2018-04-05 00:46:27 +02:00
Sebastian Muszynski
301077ded9 Xiaomi MiIO Light: White Philips Candle Light support (#13682) 2018-04-05 00:42:00 +02:00
Ville Skyttä
415af5e257 Spelling fixes (#13681) 2018-04-04 23:30:02 +02:00
Oleg
9ce02d2717 Added headers configuration variable to notify.rest component (#13674)
* Added headers configuration variable to notify.rest component

* Fix code style
2018-04-04 16:35:33 +02:00
stephanerosi
4b2fdd243a Channel up and down for webostv (#13624) 2018-04-04 15:37:14 +02:00
mountainsandcode
032d6963d8 Add regex functions as templating helpers (#13631)
* Add regex functions as templating helpers

* Add regex functions as templating helpers - Style fixed

* Templating filters, third time lucky?
2018-04-04 15:34:01 +02:00
Paulus Schoutsen
13bda2669e Bump frontend to 20180404.0 2018-04-03 16:49:13 -07:00
Adam Mills
568c6c16fa Add missing service docs for hs_color (#13667) 2018-04-03 16:05:06 -07:00
Paulus Schoutsen
92bd932679 Always enable config entries & remove config_entry_example (#13663) 2018-04-03 23:23:21 +02:00
Kevin Raddatz
bfb49c2e58 Update plex.py (#13659)
fixed IndexError on line 131
2018-04-03 18:28:42 +02:00
Fabian Affolter
89f5a938c7 Upgrade youtube_dl to 2018.04.03 (#13647) 2018-04-03 18:27:08 +02:00
Sebastian Muszynski
9ce4755f8a Xiaomi Mi WiFi Repeater 2 integration as device tracker (#13521)
* Xiaomi Mi WiFi Repeater 2 integration as device tracker

* Unused import removed

* python-miio version pinned

* Missing period added
2018-04-02 19:45:12 +02:00
Fabian Affolter
b342c43b09 Add Switzerland (#13630)
* Add Switzerland

* remove space
2018-04-02 14:02:06 +02:00
Fabian Affolter
95e98925d1 Upgrade py-cpuinfo to 4.0.0 (#13629) 2018-04-02 11:58:22 +02:00
Wolfgang Malgadey
53f08e313f changed PyTado version (#13626) 2018-04-02 10:36:38 +02:00
Niklas Wagner
9fb73c1bab Hue mireds value is actually 153 not 154 (#13601) 2018-04-02 09:45:38 +02:00
Paulus Schoutsen
98e4d514a5 Merge branch 'rc' into dev 2018-04-01 11:48:45 -07:00
Paulus Schoutsen
79eb75f26f Merge pull request #13620 from home-assistant/rc
0.66.1
2018-04-01 11:45:40 -07:00
Paulus Schoutsen
ff960c0c7a Version bump to 0.66.1 2018-04-01 11:26:54 -07:00
Martin Hjelmare
52d2139904 Fix mysensors sensor type lookup (#13574)
* Always return a safe default.
2018-04-01 11:26:33 -07:00
cdce8p
e687ca781f Add pincode fallback (#13587)
* Add pincode log statement

* Moved msg to show_setup_msg
2018-04-01 11:26:18 -07:00
Adam Mills
ff72c5e456 Fix mqtt_json color commands (#13617) 2018-04-01 11:25:51 -07:00
Adam Mills
be43c3bcfe Fix mqtt_json color commands (#13617) 2018-04-01 14:12:55 -04:00
Lewis Juggins
4ad0152a44 Bugfix for tradfri to correctly execute Command. (#13618) 2018-04-01 18:42:47 +01:00
Paulus Schoutsen
eb763764b3 Fix Hue error logging (#13616) 2018-04-01 09:03:01 -07:00
cdce8p
cd96d7b2ec Add pincode fallback (#13587)
* Add pincode log statement

* Moved msg to show_setup_msg
2018-04-01 08:38:29 -07:00
Chris Jones
dee47d50ec Use 0/1 for raspberry pi cover GPIO writes rather than true/false (#13610)
* Use 0/1 for GPIO writes rather than true/false

GPIO pins don't appear to respond to true/false writes, and this is reflected in code elsewhere. For example, in `\components\switch\rpio_gpio.py` the following code is used:

```
    def turn_on(self, **kwargs):
        """Turn the device on."""
        rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1)
        self._state = True
        self.schedule_update_ha_state()
```

This code works. Hence this PR uses 0/1 in the raspberry pi GPIO cover, instead of true/false.

* Update rpi_gpio.py
2018-04-01 08:37:03 -07:00
Martin Hjelmare
c8f2810fac Make mysensors updates and platform setup async (#13603)
* Use async updates but keep methods that interact with mysensors
  gateway thread, eg turn_on and turn_off, non async.
* Use Python 3.5 async syntax.
2018-04-01 08:36:26 -07:00
Paulus Schoutsen
ff9f500c51 Unflake folder watcher test (#13569)
* Unflake folder watcher test

* Fix tests

* Lint
2018-04-01 08:30:14 -07:00
Lewis Juggins
0c0e0c36af Re-add group polling as a fallback for observation (#13613) 2018-04-01 07:50:48 -07:00
Zhao
a8fdd76f44 Fix IMAP email message_data (#13606) 2018-04-01 12:17:26 +02:00
Daniel Høyer Iversen
45ef34ff81 Broadlink (#13585)
* Update broadlink lib

* Update broadlink lib

* requirements
2018-04-01 10:09:16 +02:00
Paulus Schoutsen
343d1384a3 Merge remote-tracking branch 'origin/rc' into dev 2018-03-31 18:13:10 -07:00
Paulus Schoutsen
9f0f7394fb Version bump frontend done right 2018-03-31 18:02:43 -07:00
Martin Hjelmare
5fce2e2b47 Fix mysensors update callback (#13602)
* Add callback annotation to mysensors dispatch callback.
2018-03-31 17:45:50 -07:00
Paulus Schoutsen
8fbef5b002 Version bump to 0.66.1b0 2018-03-31 17:44:01 -07:00
Paulus Schoutsen
3e082b5ce6 Bump frontend to 20180401.0 2018-03-31 17:43:41 -07:00
Frederik Bolding
7c99567b65 Added support for requesting RSSI values from Bluetooth devices (#12458)
* Added support for requesting RSSI values from Bluetooth devices

* Moved Bluetooth RSSI code to separate library and imported it

* Cleaned up tuple issues

* Changed concatination of mac addresses

* Changed string formatting to use new style

* Ran gen_requirements_all.py
2018-03-31 23:22:54 +02:00
Fabian Affolter
7b3d17bae4 Add mastodon (#13441)
* Add mastodon

* Move login

* Revert "Move login"

This reverts commit 2c8446f629.
2018-03-31 23:20:58 +02:00
Thibault Cohen
12affa1469 Upgrade pyhydroquebec 2.2.1 (#13586) 2018-03-31 23:16:47 +02:00
Andrew
477f7ec01e Added switch component to Amcrest IP Camera. (#12992)
* Added switch component to Amcrest IP Camera.

* Fixes to new switch component after review

* Removed redundant branching, as well as requirement declaration.

* Changes to requirements after rerunning generation script

* Minor changes
2018-03-31 23:15:25 +02:00
Myrddyn
7bf8d4ab12 Added Waze travel time sensor (#12387)
* Added Waze travel time sensor

* Update according PR comments and simplification
2018-03-31 23:01:07 +02:00
Rene Nulsch
3b4faa74a0 Remove MercedesME component (#13538) 2018-03-31 15:10:56 +02:00
Daniel Høyer Iversen
2518587534 xiaomi lib upgrade (#13577) 2018-03-31 15:08:35 +02:00
Daniel Høyer Iversen
273a43be02 rfxtrx lib 0.22.0 (#13576) 2018-03-31 15:08:04 +02:00
Martin Hjelmare
72fb64695e Fix mysensors sensor type lookup (#13574)
* Always return a safe default.
2018-03-31 15:07:29 +02:00
Tod Schmidt
bf44dc422c Added HassOpenCover and HassCloseCover intents (#13372)
* Added intents to cover

* Added test for cover intents

* Style fixes

* Reverted reversions

* Async fixes

* Woof

* Added conditional loading

* Added conditional loading

* Added conditional loading

* Moved tests, fixed logic

* Moved tests, fixed logic

* Pylint

* Pylint

* Refactored componenet registration

* Refactored componenet registration

* Lint
2018-03-30 17:22:48 -07:00
PhracturedBlue
bf58945680 Fixes #12758. Try other cameras even if one fails to initialize (#13276) 2018-03-30 15:48:31 -07:00
Paulus Schoutsen
0cfc7256ac Merge branch 'rc' into dev 2018-03-30 14:47:39 -07:00
Paulus Schoutsen
0d62f472cb Merge pull request #13554 from home-assistant/rc
0.66.0
2018-03-30 14:42:50 -07:00
Joe Lu
ad5a11ba3d Add support for Canary Flex (#13280)
Add support for Canary Flex
2018-03-30 23:38:29 +02:00
Paulus Schoutsen
4dea55b29c Version bump to 0.66.0 2018-03-30 14:11:32 -07:00
dramamoose
0f2cfe7f27 Fix FLUX_LED error when no color is set (#13527)
* Handle turn_on situation when no color is set

As is, an error gets thrown when turn_on is called without an HS value. By adding an if statement, we only try to set RGB if an HS value is applied.

* Fix Whitespace Issues

* Made Requested Changes
2018-03-30 14:10:56 -07:00
Johann Kellerman
9fc8a8f679 Check whitelisted paths #13107 (#13154) 2018-03-30 14:10:55 -07:00
dramamoose
f40efe0110 Fix FLUX_LED error when no color is set (#13527)
* Handle turn_on situation when no color is set

As is, an error gets thrown when turn_on is called without an HS value. By adding an if statement, we only try to set RGB if an HS value is applied.

* Fix Whitespace Issues

* Made Requested Changes
2018-03-30 14:10:25 -07:00
Jonas Skoogh
c361b0c450 Check_config: Handle numbers correctly when printing config (#13377) 2018-03-30 22:50:08 +02:00
Michaël Arnauts
0f24fea6bb Google Maps location sharing device tracker (#12301)
* Google Maps location sharing device tracker.

* Use ConfigType and change debug logging to _LOGGER.debug()

* Update to locationsharinglib 0.3.0

* Remove unneeded lines.

* Use hass.config.path for config file location.

* Fixed remarks

* Return boolean in setup_scanner
2018-03-30 22:47:20 +02:00
Ville Skyttä
0911166c9c Update pylint to 1.8.3 (#13544) 2018-03-30 21:34:16 +02:00
Beat
8fad97a47a Add FreeDNS component (#13526)
* Add FreeDNS component

* Implement review changes in FreeDNS component

* Implement review changes in FreeDNS component

* Implement review changes in FreeDNS component
2018-03-30 21:33:30 +02:00
Sebastian Muszynski
9cfcd38c1e Xiaomi MiIO Switch: Support for the Xiaomi Chuangmi Plug V3 (#13271)
* Device support of the Xiaomi Chuangmi Plug V3 added

* Refactoring.

* Additional attributes added.

* New miio device class used
2018-03-30 21:02:02 +02:00
ChristianKuehnel
979a8f8772 Fix BMW device tracker toggling state if vehicle tracking is disabled (#12999)
* if tracking is disabled, the position is not set in the device tracker.

This fixes an issue with a toggling vehicle state.

* removed useless attributes
2018-03-30 18:12:57 +02:00
Andrey
6314aabc6f Remove andrey-git from requirements monitoring (#13547) 2018-03-30 15:16:29 +02:00
Kane610
931bceefd9 deCONZ config entry (#13402)
* Try config entries

* Testing

* Working flow

* Config entry text strings

* Removed manual inputs for config flow

* Support unloading of config entry

* Bump requirement to v33

* Fix comments from test

* Make sure that only one deCONZ instance can be set up

* Hass doesn't support unloading platforms yet

* Modify get_api_key to be testable

* Fix hound comments

* Add test dependency

* Add test for no key

* Bump requirement to v35
Add pydeconz to list of test components

* Don't have a check in async_setup that domain exists in hass.data
2018-03-30 00:34:26 -07:00
Paulus Schoutsen
78f3e01854 Fix version bump script 2018-03-30 00:25:47 -07:00
Albert Lee
5801d78017 Implement thermostat support for Alexa (#13340)
* Implement thermostat support for Alexa

* util.temperature: Support interval conversions

* Use climate.ATTR_OPERATION_MODE for Alexa thermostat mode

* Switch coroutines to async/await

* Log all Alexa error events
2018-03-29 23:49:08 -07:00
Paulus Schoutsen
184f2be83e Convert Hue to always use config entries (#13034) 2018-03-29 20:15:40 -07:00
Paulus Schoutsen
1ae8b6ee08 Fix requirements 2018-03-29 20:02:21 -07:00
Andy Castille
170763ef2f Allow for overriding the DoorBird push notification URL in configuration (#13268)
* Allow for overriding the DoorBird push notification URL in configuration

* rename override config key
2018-03-29 20:00:26 -07:00
Johann Kellerman
507c658fe9 Check whitelisted paths #13107 (#13154) 2018-03-29 19:57:19 -07:00
Frederik Bolding
3e5462ebff Added file path validity checks to file sensor (#12505)
* Added file validity checks to file sensor

* Patched out 'is_allowed_path' for file sensor tests
2018-03-29 19:47:49 -07:00
Åskar Andersson
8617177ff1 Update rflink to 0.0.37 (#12603)
* Update requirements_all.txt

* Update rflink.py
2018-03-29 19:45:25 -07:00
Robin
df78eecc1b Adds folder_watcher component (#12918)
* Create watchdog_file_watcher.py

* Rename watchdog_file_watcher.py to folder_watcher.py

* Address a number of issues

* Adds filter

* Adds pattern matching

* Adds create_event_handler()

* Update folder_watcher.py

* Adds run_setup()

* Remove stop_watching()

* Adds shutdown()

* Update config to allow patterns on each folder

* Update to patterns from filters

* Adds watchdog

* Fix indents on schema

* Update folder_watcher.py

* Create test_file_watcher.py

* Fix lints

* Add test_invalid_path()

* Adds folder_watcher

* Update test_file_watcher.py

* Update folder_watcher.py

* Simplify config

* Adapt for new config

* Run observer.schedule() on EVENT_HOMEASSISTANT_START

* Amend Watcher removing entity and tidying startup

* Tidy config

* Rename process to on_any_event for consistency

* Rename on_any_event back to process

Using `on_any_event` resulted in 2 events being fired

* Update folder_watcher.py

* Fix return False on setup

* Update test_file_watcher.py

* Update folder_watcher.py

* Adds watchdog

* Undo adding watchdog

* Update test_file_watcher.py

* Update test_file_watcher.py

* Update test_file_watcher.py

* Update test_file_watcher.py

* Update test_file_watcher.py

* Add event

* Update test_file_watcher.py

* Update .coveragerc

* Update test_file_watcher.py

* Update test_file_watcher.py

* debug + join

* test event

* lint

* lint

* Rename test_file_watcher.py to test_folder_watcher.py

* hound

* Tidy test

* Further refine test

* Adds to test_all

* Fix test for py35

* Change test again

* Update test_folder_watcher.py

* Fix test

* Add watchdog to test

* Update folder_watcher.py

* add watchdog

* Update folder_watcher.py
2018-03-29 18:10:20 -07:00
Paulus Schoutsen
5908b55bba Fix merge conflict 2018-03-29 18:01:47 -07:00
Paulus Schoutsen
e5d76c53fb Merge remote-tracking branch 'origin/rc' into dev 2018-03-29 17:23:11 -07:00
Paulus Schoutsen
f26aff4885 Version bump to 0.66.0b3 2018-03-29 17:21:48 -07:00
Johann Kellerman
32b0712089 Don't add Falsy items to list #13412 (#13536) 2018-03-29 17:21:28 -07:00
Sebastian Muszynski
867010240a Construct version pinned (#13528)
* Construct added to the requirements

* requirements_all.txt updated
2018-03-29 17:21:28 -07:00
cdce8p
0428559f69 HomeKit: Fix setting light brightness (#13518)
* Added test
2018-03-29 17:21:28 -07:00
Tom Harris
dfd15900c7 Fix Insteon Leak Sensor (#13515)
* update leak sensor

* Fix error when insteon state type is unknown

* Bump insteon version to 0.8.3

* Update requirements all and test

* Fix requirements conflicts due to lack of commit sync

* Requirements sync

* Rerun script/gen_requirements_all.py

* Try requirements update again

* Update requirements
2018-03-29 17:21:27 -07:00
Martin Hjelmare
e993d095cb Fix mysensors light supported features (#13512)
* Different types of light should have different supported features.
2018-03-29 17:21:27 -07:00
Sebastian Muszynski
26fb3d7faa python-miio version bumped (Closes: 13449) (#13511) 2018-03-29 17:21:26 -07:00
cdce8p
b0073b437f Homekit: Fix security systems (#13499)
* Fix alarm_code=None
* Added test
2018-03-29 17:21:26 -07:00
cdce8p
e04b01daad Validate basic customize entries (#13478)
* Added schema to validate customize dictionary

* Added test
2018-03-29 17:21:25 -07:00
cdce8p
020669fc60 Homekit: Bugfix Thermostat Fahrenheit support (#13477)
* Bugfix thermostat temperature conversion
* util -> temperature_to_homekit
* util -> temperature_to_states
* util -> convert_to_float
* Added tests, deleted log msg
2018-03-29 17:21:25 -07:00
Philip Rosenberg-Watt
d897a07d0b Fix Google Calendar caching when offline (#13375)
* Fix Google Calendar caching when offline

Events from Google Calendar were not firing under the following
circumstances:

1. Start ha as normal with Google Calendar configured as per
   instructions.
2. ha loses network connectivity to Google
3. ha attempts update of Google Calendar
4. calendar/google component throws uncaught Exception causing update
   method to not return
5. (cached) Google Calendar event does not fire, remains "Off"

Catching the Exception and returning False from the update() method
causes the correct behavior (i.e., the calendar component firing the
event as scheduled using cached data).

* Add requirements

* Revert code cleanup

* Remove explicit return value from update()

* Revert "Remove explicit return value from update()"

This reverts commit 7cd77708af.

* Use MockDependency decorator

No need to whitelist google-python-api-client for a single unit test at
this point.
2018-03-29 17:21:24 -07:00
Johann Kellerman
a6b63b669e Don't add Falsy items to list #13412 (#13536) 2018-03-29 17:13:08 -07:00
Sebastian Muszynski
ab9b915731 Construct version pinned (#13528)
* Construct added to the requirements

* requirements_all.txt updated
2018-03-29 17:12:11 -07:00
Martin Hjelmare
0a0b33af03 Fix mysensors light supported features (#13512)
* Different types of light should have different supported features.
2018-03-29 17:10:56 -07:00
Tom Harris
f391cbae27 Fix Insteon Leak Sensor (#13515)
* update leak sensor

* Fix error when insteon state type is unknown

* Bump insteon version to 0.8.3

* Update requirements all and test

* Fix requirements conflicts due to lack of commit sync

* Requirements sync

* Rerun script/gen_requirements_all.py

* Try requirements update again

* Update requirements
2018-03-29 17:10:27 -07:00
Paulus Schoutsen
27865f58f1 Bump frontend to 20180330.0 2018-03-29 17:00:16 -07:00
Johann Kellerman
3fdb0002a7 Qwikswitch async refactor & sensor (#13509) 2018-03-29 23:29:46 +02:00
NovapaX
298e6eeef1 Tradfri - unique_id's and color_temp support for rgb-bulbs (#13531)
* unique_ids for tradfri lights and groups

* set color temperature on CWS bulb
Cannot set_color_temp on color bulb, needs conversion from mired to hsb

* make travis happy

* change condition so we ensure color bulbs are included, change comments.
2018-03-29 21:39:56 +02:00
cdce8p
cea2de5eb5 HomeKit: Fix setting light brightness (#13518)
* Added test
2018-03-29 18:35:57 +02:00
Fabian Affolter
3b537f6e2a Fix typos and update link (fixes #13520) (#13529) 2018-03-29 10:40:41 +02:00
Sebastian Muszynski
ef7fd9f380 python-miio version bumped (Closes: 13449) (#13511) 2018-03-28 15:55:05 -07:00
Lewis Juggins
b3b7cf3fa7 Update tradfri v5 (#11187)
* First pass to support simplified colour management in tradfri

* Fix lint

* Fix lint

* Update imports

* Prioritise brightness for transition

* Fix bug

* None check

* Bracket

* Import

* Fix bugs

* Change colour logic

* Denormalise colour

* Lint

* Fix bug

* Fix bugs, expose rgb conversion

* Fix bug

* Fix bug

* Fix bug

* Improve XY

* Improve XY

* async/wait for tradfri.

* Bump requirement

* Formatting.

* Remove comma

* Line length, shadowing

* Switch to new HS colour system, using native data from tradfri gateway.

* Lint.

* Brightness bug.

* Remove guard.

* Temp workaround for bug.

* Temp workaround for bug.

* Temp workaround for bug.

* Safety.

* Switch logic.

* Integrate latest

* Fixes.

* Fixes.

* Mired validation.

* Set bounds.

* Transition time.

* Transition time.

* Transition time.

* Fix brightness values.
2018-03-28 15:50:09 -07:00
Fabian Affolter
45ff15bc85 Upgrade aiohttp to 3.1.1 (#13510) 2018-03-28 12:45:24 +02:00
Mikael Svensson
bdb4d754ae Adds template function state_attr to get attribute from a state (#13378)
* Adds template function state_attr to get attribute from a state
Refactored is_state_attr to use new function
Adds tests for state_attr

* Fixes line too long and test bug

* Fixes pylint error

* Fixes tests and D401 lint error
2018-03-28 00:04:18 -07:00
Fabian Affolter
00c6df54b2 Upgrade slacker to 0.9.65 (#13496) 2018-03-28 08:27:56 +02:00
cdce8p
2bebfec3a6 Homekit: Fix security systems (#13499)
* Fix alarm_code=None
* Added test
2018-03-27 23:39:25 +02:00
Sebastian Muszynski
264be67787 New service added to control the power mode of the yeelight (#13267)
* New service added to control the power mode of the yeelight

* Debug output removed.

* Strict validation of the available power modes

* Service description added

* Service parameter name fixed
2018-03-27 20:29:18 +02:00
Fabian Affolter
06aded1a4d Upgrade python-mystrom to 0.4.2 (#13485) 2018-03-27 13:09:01 +02:00
cdce8p
9eda04b787 Homekit: Bugfix Thermostat Fahrenheit support (#13477)
* Bugfix thermostat temperature conversion
* util -> temperature_to_homekit
* util -> temperature_to_states
* util -> convert_to_float
* Added tests, deleted log msg
2018-03-27 11:31:18 +02:00
cdce8p
8a0facb747 Validate basic customize entries (#13478)
* Added schema to validate customize dictionary

* Added test
2018-03-26 19:50:29 -07:00
Philip Rosenberg-Watt
81cf0dacfe Fix Google Calendar caching when offline (#13375)
* Fix Google Calendar caching when offline

Events from Google Calendar were not firing under the following
circumstances:

1. Start ha as normal with Google Calendar configured as per
   instructions.
2. ha loses network connectivity to Google
3. ha attempts update of Google Calendar
4. calendar/google component throws uncaught Exception causing update
   method to not return
5. (cached) Google Calendar event does not fire, remains "Off"

Catching the Exception and returning False from the update() method
causes the correct behavior (i.e., the calendar component firing the
event as scheduled using cached data).

* Add requirements

* Revert code cleanup

* Remove explicit return value from update()

* Revert "Remove explicit return value from update()"

This reverts commit 7cd77708af.

* Use MockDependency decorator

No need to whitelist google-python-api-client for a single unit test at
this point.
2018-03-26 18:10:22 -07:00
Paulus Schoutsen
e1d2d201c8 Merge branch 'rc' into dev 2018-03-26 16:24:48 -07:00
Paulus Schoutsen
3639a4470c Use twine for release 2018-03-26 16:16:42 -07:00
Paulus Schoutsen
94d9aa0c5f Bump version to 0.66.0.b2 2018-03-26 16:10:03 -07:00
Paulus Schoutsen
a06eea444a version should contain just 'b' not 'beta' (#13476) 2018-03-26 16:09:18 -07:00
Paulus Schoutsen
ce3a5972c7 Upgrade aiohue and fix race condition (#13475)
* Bump aiohue to 1.3

* Store bridge in hass.data before setting up platform

* Fix tests
2018-03-26 16:09:18 -07:00
Fabian Affolter
f48ce3d437 Fix ID (fixes #13444) (#13471) 2018-03-26 16:09:17 -07:00
Paulus Schoutsen
dfe3219f3f Hue: Convert XY to HS color if HS not present (#13465)
* Hue: Convert XY to HS color if HS not present

* Revert change to test

* Address comments

* Lint
2018-03-26 16:09:17 -07:00
Fabian Affolter
254256c08f Fix ID (fixes #13444) (#13471) 2018-03-26 16:08:44 -07:00
Paulus Schoutsen
f1d37fc849 Upgrade aiohue and fix race condition (#13475)
* Bump aiohue to 1.3

* Store bridge in hass.data before setting up platform

* Fix tests
2018-03-26 16:07:22 -07:00
Paulus Schoutsen
08bcf84170 version should contain just 'b' not 'beta' (#13476) 2018-03-26 14:55:09 -07:00
Paulus Schoutsen
24e0bb198a Hue: Convert XY to HS color if HS not present (#13465)
* Hue: Convert XY to HS color if HS not present

* Revert change to test

* Address comments

* Lint
2018-03-26 14:00:56 -07:00
phileaton
263dbe5d81 Update total_connect_client to 0.17 for Honeywell L5100-WiFi Support (#13473)
* Update total_connect_client to 0.17

* Delete tqdm.1
2018-03-26 21:32:38 +02:00
Anders Melchiorsen
3e6f4d0e5a [RFC] Update issue template (#12989)
* Update issue template

* Any release
2018-03-26 21:21:18 +02:00
c727
181e68b027 Add more info to issue template (#12955)
* Update ISSUE_TEMPLATE.md

* Minumum supported version is Python 3.5.3

* typo

* Feedback

* Feedback

* Address comments
2018-03-26 19:22:05 +02:00
Lindsay Ward
2e3ec121d1 Update yeelightsunflower to 0.0.10 (#13448)
* Update yeelightsunflower to 0.0.10

* Update yeelightsunflower platform to 0.0.10
2018-03-26 18:27:53 +02:00
Dan Nixon
a6e455a070 Make Telnet Switch value template optional (#13433)
When no statis command is defined a value template does nothing so
should not have to be provided.
2018-03-26 18:22:21 +02:00
Paulus Schoutsen
a08e5efe53 Merge branch 'rc' into dev 2018-03-25 18:25:25 -07:00
Paulus Schoutsen
a507ed0af8 Version bump to 0.66.0.beta1 2018-03-25 18:24:16 -07:00
Beat
068b037944 Fix encoding errors in mikrotik device tracker (#13464) 2018-03-25 18:23:32 -07:00
Cedric Van Goethem
38d2702e3c Add extra check for ESSID field in case there's a wired connection (#13459) 2018-03-25 18:23:32 -07:00
Paulus Schoutsen
93b9ec0b0f Add version bump script (#13447)
* Add version bump script

* Lint
2018-03-25 18:23:31 -07:00
Anders Melchiorsen
22cefc7e64 Improve detection of entity names in templates (#13432)
* Improve detection of entity names in templates

* Only test variables
2018-03-25 18:23:31 -07:00
cdce8p
60f6109cbf HomeKit: Bugfix & improved logging (#13431)
* Bugfix & improved logging

* Removed logging statements

* Removed logging test
2018-03-25 18:23:30 -07:00
a-andre
24d299e266 Hyperion: fix typo (#13429) 2018-03-25 18:23:30 -07:00
Anders Melchiorsen
444805df10 LimitlessLED hs_color fixes (#13425) 2018-03-25 18:23:30 -07:00
Anders Melchiorsen
a08293cff7 Log invalid templates in script delays (#13423)
* Log invalid templates in script delays

* Abort on error

* Remove unused import
2018-03-25 18:23:29 -07:00
Patrick Hofmann
0d48a8eec6 Security fix & lock for HomeMatic (#11980)
* HomeMatic KeyMatic device become a real lock component

* Adds supported features to lock component.

Locks may are capable to open the door latch.
If component is support it, the SUPPORT_OPENING bitmask can be supplied in the supported_features property.

* hound improvements.

* Travis improvements.

* Improvements from review process

* Simplifies is_locked method

* Adds an openable lock in the lock demo component

* removes blank line

* Adds test for openable demo lock and lint and reviewer improvements.

* adds new line...

* Comment end with a period.

* Additional blank line.

* Mock service based testing, lint fixes

* Update description
2018-03-25 18:23:29 -07:00
Paulus Schoutsen
8a204fd15b Bump frontend to 20180326.0 2018-03-25 18:10:59 -07:00
a-andre
1887bac37e Hyperion: fix typo (#13429) 2018-03-25 18:07:26 -07:00
Paulus Schoutsen
d6af26b589 Add version bump script (#13447)
* Add version bump script

* Lint
2018-03-25 18:04:20 -07:00
Cedric Van Goethem
a5ae77ab93 Add extra check for ESSID field in case there's a wired connection (#13459) 2018-03-25 18:03:23 -07:00
Beat
8ab5978db3 Fix encoding errors in mikrotik device tracker (#13464) 2018-03-25 18:02:21 -07:00
Johann Kellerman
3e3f710b12 Qwikswitch async & updates (#12641) 2018-03-25 23:32:13 +02:00
Patrick Hofmann
6d20a84f0e Security fix & lock for HomeMatic (#11980)
* HomeMatic KeyMatic device become a real lock component

* Adds supported features to lock component.

Locks may are capable to open the door latch.
If component is support it, the SUPPORT_OPENING bitmask can be supplied in the supported_features property.

* hound improvements.

* Travis improvements.

* Improvements from review process

* Simplifies is_locked method

* Adds an openable lock in the lock demo component

* removes blank line

* Adds test for openable demo lock and lint and reviewer improvements.

* adds new line...

* Comment end with a period.

* Additional blank line.

* Mock service based testing, lint fixes

* Update description
2018-03-25 23:25:28 +02:00
Fabian Affolter
b996632965 Upgrade aiohttp to 3.1.0 (#13451) 2018-03-25 14:25:00 +02:00
Fabian Affolter
eaf81150ea Upgrade keyring to 12.0.0 and keyrings.alt to 3.0 (#13452) 2018-03-25 14:23:53 +02:00
cdce8p
7db37a3834 HomeKit: Bugfix & improved logging (#13431)
* Bugfix & improved logging

* Removed logging statements

* Removed logging test
2018-03-25 12:53:15 +02:00
Anders Melchiorsen
55daea5169 Improve detection of entity names in templates (#13432)
* Improve detection of entity names in templates

* Only test variables
2018-03-25 12:51:11 +02:00
Anders Melchiorsen
594a5b7d29 LimitlessLED hs_color fixes (#13425) 2018-03-25 00:47:10 -07:00
Alan Tse
3a765692e7 Fixing odometer to display km (#13427) 2018-03-25 00:46:47 -07:00
Marc Forth
2d2e8034d6 Removed the google home warning from emulated_hue (#13436)
* Removed the google home warning from emulated_hue

* Update test_init.py

* Update test_init.py
2018-03-25 00:45:25 -07:00
Teemu R
f3ccbda054 Bump songpal version, fixes lots of issues mentioned in #13022 (#13440) 2018-03-25 08:24:03 +02:00
Anders Melchiorsen
7166d53e2b Log invalid templates in script delays (#13423)
* Log invalid templates in script delays

* Abort on error

* Remove unused import
2018-03-25 01:12:26 +01:00
Sebastian Muszynski
e36f27d6fd Xiaomi MiIO Fan: Xiaomi Air Humidifier integration (#12627)
* Device support for the Xiaomi Air Humidifier.

* Requirements updated.

* "continuation line under-indented for visual indent" fixed.

* Make hound happy.

* Inadvertently added light.xiaomi_miio component removed from PR.

* Service descriptions added.

* One of the pylint errors fixed.

* Redundancy removed.

* pylint: disable=no-self-use added. The method signature is important here.

* Pylint fixed.

* Use a unique data key per domain.

* Review incorporated.

* Map of available attributes added.

* Pylint fixed.
Attribute "volume" added.

* Don't use the support flag bit mask as model identifier.
Determine support features and attributes at the constructor.
Use starred expressions at dicts instead of copies.

* Blank line removed.

* Use Async / await syntax.

* Make hound happy.

* Xiaomi Air Humidifier CA support added.

* Duplicate method removed.

* Air Purifier V3 support added.

* Don't abuse the system property supported_features anymore.

* python-miio version bumped.

* Clean-up.

* Additional supported features refactoring completed.

* Additional supported features renamed properly.

* Unique id added.

* Device unavailable handling improved.

* Refactoring.

* Missed const updated.

* Incomplete Air Humidifier CA support fixed.

* Review incorporated

* The Air Humidifier CA supports the operation mode "auto" - the standard version doesn't

* Attributes are part of the common set already

* Revert "Attributes are part of the common set already"

This reverts commit 40b443eba0.

* Comment added

* Service description of the set_dry_{on,off} service added

* Typo fixed
2018-03-24 23:04:43 +01:00
Colin O'Dell
11930d5f20 QNAP updates (#13435)
* Add @colinodell to CODEOWNERS for qnap sensor

* Bump qnapstats library to 0.2.5

This release adds better error handling for sharenames with no folder
2018-03-24 22:13:12 +01:00
Colin O'Dell
df35159cb4 Add code owner for Manual Alarm with MQTT Control (#13438) 2018-03-24 21:33:49 +01:00
ChristianKuehnel
4d52875229 Update to new "b2vapi" of BMW ConnectedDrive (#13305)
* updated to new "b2vapi" of bimmer_connected

* updated requirements_all.txt

* updated 2 more vehicle names after rebase

* cleanup of import statements

* found one more broken name...

* removed unused constant

* cleanup of import statements 2
2018-03-24 12:16:49 +01:00
Fabian Affolter
8bd5f66c57 Upgrade mypy to 0.580 (#13420) 2018-03-23 23:50:32 +01:00
Franck Nijhof
872b6cf16b Updates default Pilight port number (#13419) 2018-03-23 23:22:33 +01:00
Paulus Schoutsen
8e14e803cb Fix release script 2018-03-23 14:27:21 -07:00
Paulus Schoutsen
101b39300b Version bump to 0.66.0.beta0 2018-03-23 14:17:36 -07:00
Paulus Schoutsen
b159484a79 Version bump to 0.67.0.dev0 2018-03-23 14:16:17 -07:00
Paulus Schoutsen
725e1ddfc1 Update translations 2018-03-23 14:15:44 -07:00
Paulus Schoutsen
a17e60208d Update translations 2018-03-23 14:15:31 -07:00
Otto Winter
6a625bdb37 Cast Integration Cleanup (#13275)
* Cast Integration Cleanup

* Fix long line

* Fixes and logging

* Fix tests

* Lint

* Report unknown state with None

* Lint

* Switch to async_add_job

Gets rid of those pesky "Setup of platform cast is taking over 10 seconds." messages.

* Re-introduce PlatformNotReady

* Add tests

* Remove unnecessary checks

* Test PlatformNotReady

* Fix async in sync context

* Blocking update

It's not using async anyway

* Upgrade pychromecast to 2.1.0

* Make reviewing easier

I like "protected" access, but I like reviewing more :)

* Make reviewing even easier :)

* Comment tests
2018-03-23 14:02:52 -07:00
Matt Hamrick
630734ca15 Switched values to downcase. (#13406) 2018-03-23 13:54:36 -07:00
cdce8p
7fd687f59c Fix current_cover_position (#13135) 2018-03-23 13:54:19 -07:00
Paulus Schoutsen
4bd6776443 Google assistant sync (#13392)
* Add Google Assistant Sync API

* Update const.py

* Async/await
2018-03-23 12:13:52 -07:00
Sebastian Muszynski
2532d67b9a Add send sticker service to telegram bot (#13387)
* Add send sticker service to telegram bot

* A caption is not supported
2018-03-23 19:16:57 +01:00
Mattias Welponer
df8596e896 Cleanup homematicip_cloud (#13356)
* Cleanup and proposed changes from MartinHjelmare

* Removed coroutine decorator from async_added_to_hass

* Added blank line

* Fix of component url

* Fix of component url

* Fix url of the sensor component
2018-03-23 11:05:02 -07:00
Adam Mills
2497dd5e33 Hue: Use the currently active color mode (#13376)
* Hue: Use the currently active color mode

* Round hue/sat colors before reporting to API

* .gitignore cache fix
2018-03-23 11:01:40 -07:00
cdce8p
553920780f Added default return value for HS_Color (#13395) 2018-03-23 10:22:48 -07:00
Jerad Meisner
8852e52601 Switched to async/await. Bumped pyxeoma version (#13404) 2018-03-23 10:22:01 -07:00
Martin Hjelmare
23165cbd1a Enhance mysensors binary sensor device classes (#13367) 2018-03-23 18:11:53 +01:00
Philip Rosenberg-Watt
23f06b0040 Cache LaMetric devices for offline use (#13379)
If the connection to the LaMetric server fails, we should still be able
to send notifications to known and reachable devices.
2018-03-23 18:10:52 +01:00
Anders Melchiorsen
5ec6f25d4e Fix Sonos playing Sveriges Radio (#13401) 2018-03-23 18:09:17 +01:00
cgtobi
79c9d3ba10 Fix incorrect unit of measurement for precip_intensity. (#13415) 2018-03-23 18:08:16 +01:00
Martin Hjelmare
ba7178dc0c Enhance mysensors sensor units and icons (#13365) 2018-03-23 18:06:07 +01:00
Erik Eriksson
2c7bc6eaf8 Support setting icon when configuring MQTT entity (#13304) 2018-03-23 11:30:44 +01:00
Gerard
c50b00226c Avoid breaking change for BMW ConnectedDrive sensors in #12591 (#13380) 2018-03-23 07:32:33 +01:00
Paulus Schoutsen
fb1fafefab Include all config flow translations with backend translations (#13394) 2018-03-22 12:21:33 -07:00
Jeroen ter Heerdt
98620d8ce8 Fixing Egardia 'home armed' state not shown correctly. (#13335)
* Fixing Egardia 'home armed' state not shown correctly.

* Updating requirements_all.

* Adding DEPEDENCY list to Egardia components.

* updating requirements_all
2018-03-22 10:53:52 -07:00
Paulus Schoutsen
d385e9645d Merge remote-tracking branch 'origin/master' into dev 2018-03-22 10:20:35 -07:00
cgtobi
e9cdbe5d8c Add language parameter to darksky sensor (#13297) 2018-03-22 13:34:02 +01:00
Paulus Schoutsen
6e75c5427c Update frontend to 20180322.0 2018-03-21 21:22:16 -07:00
Diogo Gomes
1676df6a5f Mediaroom async (#13321)
* Initial commit on new asyncio based version

* Async version

* Lint

* updated to lasted pymediaroom version

* bump version

* optimistic state updates

* bump version

* Moved class import to method import

* async schedule and name correction

* Addresses @balloob comments

* missed fixes

* no unique_id for configuration based STB

* hound

* handle 2 mediaroom platforms
2018-03-21 18:06:41 -07:00
Martin Hjelmare
17cbd0f3c9 Add watt to mysensors switch attributes (#13370) 2018-03-21 23:55:49 +01:00
Martin Hjelmare
2d7d8848cb Fix mysensors RGBW (#13364)
Tuple doesn't have append method.
2018-03-21 23:48:50 +01:00
Thomas Svedberg
3939460814 Add the possibility to filter on line(s) in Västtrafik Public Transport sensor (#13317)
* Add the possibility to filter on line(s) in Västtrafik Public Transport sensor

Add a config entry "lines" to be able to filter departures on line(s) for the
vasttrafik sensor.

* Change log level to debug if no departures found.

* Remove extra None argument from dict().get() calls as it is already the default.

* Ensure "lines" is a list of strings. Also fix an indentation error.

* Correct to long line
2018-03-21 15:21:51 -07:00
maxclaey
36bc7f8175 Configuration options for IFTTT alarm control panel (#13352)
* Improvements

* Use optimistic instead of await callback

* Fix default value for optimistic
2018-03-21 12:29:58 -07:00
maxclaey
0396725fe9 Homekit Bugfix: Use get instead of indexing (#13353)
Fixes bug for alarm_control_panel if not code is required.
2018-03-21 19:06:46 +01:00
Paulus Schoutsen
c3974e540b Merge pull request #13369 from home-assistant/release-0-65-6
0.65.6
2018-03-21 10:59:58 -07:00
Scott Reston
74c249e57d Fix retrieval of track URL into medi_content_id (#13333)
'current.item' was returning blank.
2018-03-21 10:44:53 -07:00
Paulus Schoutsen
da4e630f54 Fix tests 2018-03-21 10:38:06 -07:00
Paulus Schoutsen
ffbafa687a Do not include unavailable entities in Google Assistant SYNC (#13358) 2018-03-21 10:14:56 -07:00
Anders Melchiorsen
7718f70c5f Fix Sonos radio stations with ampersand (#13293) 2018-03-21 10:14:56 -07:00
Anders Melchiorsen
0de2681783 Fix Sonos join/unjoin in scripts (#13248) 2018-03-21 10:14:55 -07:00
Paulus Schoutsen
7e08e8bd51 Tado: don't reference unset hass var (#13237)
Tado: don't reference unset hass var
2018-03-21 10:14:54 -07:00
PhracturedBlue
2388d62755 More robust MJPEG parser. Fixes #13138. (#13226)
* More robust MJPEG parser. Fixes ##13138.

* Reimplement image extraction from mjpeg without ascy generator to support python 3.5
2018-03-21 10:14:54 -07:00
Paulus Schoutsen
fab958d789 Version bump to 0.65.6 2018-03-21 10:14:28 -07:00
Anders Melchiorsen
f8127a3902 Add a polling fallback for Sonos (#13310)
* Prepare for poll

* Add a polling fallback for Sonos
2018-03-20 19:27:07 -07:00
Martin Hjelmare
3426487277 Fix mysensors light turn on hs color (#13349) 2018-03-20 18:26:56 -07:00
Paulus Schoutsen
cfb0b00c0c Do not include unavailable entities in Google Assistant SYNC (#13358) 2018-03-20 18:09:34 -07:00
Anders Melchiorsen
852eef8046 Fix Sonos radio stations with ampersand (#13293) 2018-03-20 18:05:03 -07:00
Daniel Perna
05c9c57500 Update pyhomematic to 0.1.40 (#13354)
* Update __init__.py

* Update requirements_all.txt
2018-03-20 20:32:59 +01:00
John Arild Berentsen
5c4529d044 Bugfix: Zwave set_config_parameter failed when config list contained int (#13301)
* Cast list and bool options to STR

* Add button options to STR

* Add TYPE_BUTTON to value types

* Adjust comparison

* Remove Logging

* Remove Empty line

* Update tests

* Update tests

* Mistake
2018-03-20 14:04:24 +01:00
Daniel Høyer Iversen
3fa080a795 Add min and max price as attribute for Tibber sensor (#13313) 2018-03-20 08:46:10 +01:00
Sergio Viudes
0977be1842 Added switch for DoorBird second relay (#13339) 2018-03-20 08:43:31 +01:00
Johann Kellerman
4270bc7abb Perform check_config service in current process (#13017)
* Perform check_config service in current process

* feedback
2018-03-19 14:20:04 -07:00
Ryan McLean
a04c6d5830 Plex unavailable client cleanup (#13156)
* Added timestamp for when device was marked unavailable

* protect time 1st marked

* client removal, no errors

* Optional removal interval added

* Linting error fix

* Implemented guard to prevent indentation
Removed vars in favour of inline calcs

* Using hass.add_job() for cleanup

* Lint

* Revert removal interval to 600sec

* Changed datetime to hass implementation

* Forgot to include one of the references to dt
2018-03-19 14:15:21 -07:00
Per Osbäck
f287955422 zha: catch the exception from bellows if a device isn't available. (#13314)
* catch the exception from bellows if a device isn't available.

* fix import of zigpy exception

* fix lint import
2018-03-19 14:12:53 -07:00
Qxlkdr
2bc7e58780 Add trafikverket_weatherstation sensor platform (#12115)
* Create trafikverket_weatherstation.py

Created PR 12111 but due to permission issue I'm creating a new fork and PR.

* Added dot

Added dot to the first (second) row of the file, after the description.

* Corrections based on feedback

Done:
- Run flake8 before this commit
- Fixed invalid variables
- Shortened the xml variable/query via if statement (air_vs_road)
- Moved imports if update() to top of the file
- Imported CONF_API_KEY and CONF_TYPE
- Updated documentation (api_key): home-assistant/home-assistant.github.io#4562

Actions left:
- Error handling
- Request timeout
- Add sensor (file) to .coveragerc

* Multiple corrections

Done:
- Executed pylint and flake8 tests before commit
- Fixed import order
- Implemented request timeout
- Used variable air_vs_road in the return as well

Actions left:
- Error handling
- Add sensor (file) to .coveragerc

* Error handling

Done:
- Error handling for network
- Error handling for JSON response

* Added trafikverket_weatherstation.py

Added trafikverket_weatherstation.py in correct order.

* Road as default

Changed if statement to check for 'road' which means it will always defaulting to 'air' in other cases. Even if it will only accept 'air' and 'road' from the PLATFORM_SCHEMA.

* Updated variable names

Updated variable names to be more understandable as requested by @MartinHjelmare

* Standard Libraries

Grouped Standard Libraries

* Return None

Changed return None to only return as suggested.
2018-03-19 21:38:07 +01:00
Otto Winter
947218d51c pytest 3.4.0 cache gitignore (#13308) 2018-03-19 10:47:10 +01:00
Diogo Gomes
49683181d1 Superfluous None (#13326) 2018-03-18 20:42:23 -07:00
Adam Mills
89c7c80e42 Use hue/sat as internal light color interface (#11288)
* Accept and report both xy and RGB color for lights

* Fix demo light supported_features

* Add new XY color util functions

* Always make color changes available as xy and RGB

* Always expose color as RGB and XY

* Consolidate color supported_features

* Test fixes

* Additional test fix

* Use hue/sat as the hass core color interface

* Tests updates

* Assume MQTT RGB devices need full RGB brightness

* Convert new platforms

* More migration

* Use float for HS API

* Fix backwards conversion for KNX lights

* Adjust limitless min saturation for new scale
2018-03-18 15:00:29 -07:00
nielstron
6b059489a6 Adding a discoverable Samsung Syncthru Printer sensor platform (#13134)
* Added a simple component to support the BLNET

Adds a component based on pyblnet, that hooks up the blnet to home assistant

* Adds support for custimzation of blnet sensor devices

* Setting up blnet as a platfrom

* Updated use of state_attributes

Now the friendly_name (and for digital values the mode) is set in the state_attributes whereas the name is defined as "blnet_(analog|digital)_{sensornumber}" so you can reliably add them to groups.

* Added support for the SyncThru printer web service

* Added pysyncthru to the requirements

* Changed to Dependencis, import inside setup_platform

* Switch back to REQUIREMENTS

Looks like DEPENDENCIES is not meant for python packages but for other HA components

* Fixed access to _attributes

* Final fix

* Several Bugfixes

When the printer goes offline, the last state will be kept.
Also now checks if the printer is reachable upon setup

* Register syncthru as discoverable

* Included possible conditions to monitor

* Split the printer sensor in several seperate sensor entities

* Fixed bug at sensor creation, pep8 conform

* Bugfix

* Bugfix

* Removed Blnet components

* Fixed unused import

* Renamed discoverable to samsung_printer

* Removed unused Attribute _friendly_name

* Inserted missing space

* Pinned requirements and added to coveragerc

* Reduced redundancy by condensing into multiple sub-classes

* Fixed indentation

* Fixed super constructor calls

* Fixed super constructor calls

* Fixed format

* Resolving style issues and using name instead of friendly_name

* Pinned pysyncthru in requirements_all, having trouble with friendly_name

* Iterating over dictionary instead of dict.keys()

* ran gen_reqirements_all.py

* Fixed flake 8 issues

* Added a simple component to support the BLNET

Adds a component based on pyblnet, that hooks up the blnet to home assistant

* Implemented requested changes

* raised dependecies to pysyncthru version that has timeouts

* Raised required version for full timeout support

* Adds support for custimzation of blnet sensor devices

* Setting up blnet as a platfrom

* Updated use of state_attributes

Now the friendly_name (and for digital values the mode) is set in the state_attributes whereas the name is defined as "blnet_(analog|digital)_{sensornumber}" so you can reliably add them to groups.

* Added support for the SyncThru printer web service

* Added pysyncthru to the requirements

* Removed Blnet components

* Pinned requirements and added to coveragerc

* Fixed indentation

* Fixed format

* Pinned pysyncthru in requirements_all, having trouble with friendly_name

* ran gen_reqirements_all.py

* Updated requirements_all

* Renamed sensor objects, removed passing of hass entity

* Removed merge artifacts

* Reset syncthru to newest state

* Updated requirements_all

* switched to using the newest version of pysyncthru

* Sorted coveragerc
2018-03-18 09:26:33 -07:00
Erik Eriksson
1cbf9792d7 Support MQTT Lock discovery (#13303) 2018-03-18 09:26:07 -07:00
Kevin Raddatz
437ffc8337 Update plex.py (#12157)
* Update plex.py

show information about media depending if it is a movie or an episode
set time_between_scans to 10 s to match with plex media_player component

* Update plex.py

lint

* Update plex.py

linting

* Update plex.py

linting

* Update plex.py

linting

* Update plex.py

added catch  for tracks and everything else
if no release year is given, instead of () it now show nothing

* Update plex.py

Remove the album year to match with the Plex UI

* Update README.rst

* Update README.rst

* Update plex.py

reformat code to make it more readable
recorded tv shows might not have episode numbers assigned -> check before adding to title

* Update plex.py

cleanup excessive whitespace

* Update plex.py

cleanup excessive whitespace
2018-03-18 09:25:25 -07:00
Igor Bernstein
9cb3c9034f Zigbee fan (#12289)
* wip: initial control

* fix initial state

* cosmetic cleanup

* doc typo

* lint

* fixes

* fix unknown bug

* Lint
2018-03-18 09:17:56 -07:00
uchagani
1dcc51cbdf Add ecobee fan mode (#12732)
* add ability to set fan on

* add tests and change "not on" status to "auto"

* hound fix

* more hounds

* I don't understand new lines

* fix linting errors

* more linting fixes

* change method signature

* lint fixes

* hopefully last lint fix

* correct temp ranges according to ecobee API docs

* update dependency to latest version

* update tests with values from new temp logic

* fix linting issue

* more linting fixes

* add SUPPORT_FAN_MODE to capabilities

* add fan_list to attributes.
restore current fan state to OFF if fan is not running.
change target high/low temps from null to target temp when not in auto mode.
change target temp from null to high/low temp when in auto mode
change mode attribute to climate_mode for consistency with other lists.

* remove unused import

* simplify logic

* lint fixes

* revert change for target temps
2018-03-18 09:02:07 -07:00
maxclaey
022d8fb816 Support for security systems controlled by IFTTT (#12975)
* Add IFTTT alarm control panel

* Update .coveragerc

* Add support for code

* Bugfix

* Fix logging problem

* Pin requirements

* Update requirements_all.txt

* Fix lint errors

* Use ifttt component as a dependency instead of interacting with ifttt manually
Take into account review comments

* No default value for code

* Take into account review comments

* Provide a "push_alarm_state" service to change the state from IFTTT

* Add service description

* Fix @balloob review comments. Thanks!

* Fix service description name
2018-03-18 09:00:08 -07:00
Diogo Gomes
1e17b2fd63 Added Time based SMA to Filter Sensor (#13104)
* Added Time based SMA

* move "now" to _filter_state()

* Addressed comments

* fix long line

* type and name

* # pylint: disable=redefined-builtin

* added test
2018-03-18 08:58:52 -07:00
Mattias Welponer
b45dad507a Add initial support fo HomematicIP components (#12761)
* Add initial support fo HomematicIP components

* Fix module import

* Update reqirments file as well

* Added HomematicIP files

* Update to homematicip

* Code cleanup based on highligted issues

* Update of reqiremnets file as well

* Fix dispatcher usage

* Rename homematicip to homematicip_cloud
2018-03-18 08:57:53 -07:00
Fabian Affolter
8ed3024026 Upgrade async_timeout to 2.0.1 (#13290) 2018-03-17 17:37:31 -07:00
cburgess
d042b3d7d1 Update to latest python-nest (#12590)
Due to an upstream bug some devices will be assigned a where_id that is not
visible in the nest API. As a result we can't get a friendly name for the
where_id.

A workaround has been released for python-nest in version 3.7.0. Update the
home assistant requirements to python-nest==3.7.0 to work around this issue.

References:
https://nestdevelopers.io/t/missing-where-name-from-some-devices/1202
https://github.com/jkoelker/python-nest/issues/127
https://github.com/jkoelker/python-nest/pull/128

Fixes #12589
Fixes #12950
Fixes #13074
2018-03-17 17:35:16 -07:00
Pascal Vizeli
4d3743f3f7 Delete .gitmodules (#13295) 2018-03-17 19:08:52 +01:00
cgtobi
181eca4b45 Upgrade python-forecastio to 1.4.0 (#13282)
* Upgrade python-forecastio to 1.4.0

* Upgrade python-forecastio to 1.4.0 for sensor as well.
2018-03-17 17:43:07 +01:00
Fabian Affolter
dbc59ad1a7 Upgrade python-telegram-bot to 10.0.1 (#13294) 2018-03-17 17:41:10 +01:00
Fabian Affolter
82f59ba984 Upgrade numpy to 1.14.2 (#13291) 2018-03-17 17:40:31 +01:00
Fabian Affolter
d35077271d Upgrade TwitterAPI to 2.5.0 (#13287) 2018-03-17 17:40:03 +01:00
Fabian Affolter
aec61b7c86 Upgrade sqlalchemy to 1.2.5 (#13292) 2018-03-17 17:39:24 +01:00
Fabian Affolter
e01a0f91d6 Upgrade aiohttp_cors to 0.7.0 (#13289) 2018-03-17 17:37:53 +01:00
Fabian Affolter
8fed405da7 Upgrade aiohttp to 3.0.9 (#13288) 2018-03-17 17:37:09 +01:00
Otto Winter
3442b6741d Fix WUnderground duplicate entity ids (#13285)
* Fix WUnderground duplicate entity ids

* Entity Namespace
2018-03-17 13:14:53 +01:00
Otto Winter
f5093b474a Python 3.5 async with (#13283) 2018-03-17 12:27:21 +01:00
thrawnarn
05676ba18b Changed to async/await (#13246)
* Changed to async/await

* Hound fixes

* Lint fixes

* Changed sleep
2018-03-17 12:14:01 +01:00
Sebastian Muszynski
66c6f9cdd6 Unused xiaomi miio sensor method removed (#13281)
* Unused method removed.

* remove unused import
2018-03-17 11:51:40 +01:00
Paulus Schoutsen
5a9013cda5 Refactor Hue: phue -> aiohue (#13043)
* phue -> aiohue

* Clean up

* Fix config

* Address comments

* Typo

* Fix rebase error

* Mark light as unavailable when bridge is disconnected

* Tests

* Make Throttle work with double delay and async

* Rework update logic

* Don't resolve host to IP

* Clarify comment

* No longer do unnecessary updates

* Add more doc

* Another comment update

* Wrap up tests

* Lint

* Fix tests

* PyLint does not like mix 'n match async and coroutine

* Lint

* Update aiohue to 1.2

* Lint

* Fix await MagicMock
2018-03-16 20:27:05 -07:00
Paulus Schoutsen
d78e75db66 Bump frontend to 20180316.0 2018-03-16 15:39:30 -07:00
Sebastian Muszynski
d04ba3f86d Xiaomi MiIO Sensor: Xiaomi Air Quality Monitor (PM2.5) integration (#13264)
* Xiaomi MiIO Sensor: Xiaomi Air Quality Monitor (PM2.5) integration.

* Missing newline added.

* Use a unique data key per domain.

* turn_{on,off} service moved to __init__.py.

* All sensors group added.

* Sensor is a ToggleEntity now.

* is_on property added.

* Use Async / await syntax.

* Make hound happy.

* Unique id added.

* Turn on/off service removed from abstract sensor.

* Turn on/off methods removed.
Device unavailable handling improved.

* Unused import removed.

* Sensor migrated back to an entity.

* Rebased and requirements updated.
2018-03-16 22:13:04 +01:00
Sebastian Muszynski
ed6cd0ccfa Xiaomi MiIO Remote: Unique id added (#13266)
* Unique id added.

* Provide the exception as "ex"
2018-03-16 21:15:23 +01:00
Jens Timmerman
88d2a6ab80 Fix guide link in CONTRIBUTING.md (#13272) 2018-03-16 21:13:32 +01:00
Sebastian Muszynski
fe7012549e Xiaomi MiIO light: Philips Eyecare Smart Lamp 2 integration (#12883)
* Xiaomi Philips Eyecare Smart Lamp 2 support added.

* Blank lines removed.

* Pylint errors fixed.

* Abstract light introduced.

* Smart night light mode renamed.

* Use the conventional power on/off methods again.

* Eyecare mode service added.

* Eyecare mode attribute added.

* Name of the ambient light entity fixed.

* Reuse of the same local variable name within the same scope fixed.

* Use Async / await syntax.

* Missed method fixed.

* Make hound happy.

* Don't abuse the system property supported_features anymore.

* Make hound happy.

* Wrong hanging indentation fixed.
Unnecessary parens after 'return' keyword fixed.

* Refactoring.

* Additional supported features bit mask removed as long as the differences of the supported devices are simple.

* Support for Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp added.

* Docstrings updated.
Refactoring.

* Unique id added.

* Filter service calls. Dummy methods removed.

* Device available handling improved.

* super() used for calling the parent class

* Self removed from super().
2018-03-16 20:59:18 +01:00
Sebastian Muszynski
f013619e69 Xiaomi MiIO Switch: Power Strip support improved (#12917)
* Xiaomi MiIO Switch: Power Strip support improved.

* New service descriptions added.

* Make hound happy.

* Pylint fixed.

* Use Async / await syntax.

* Missed method fixed.

* Make hound happy.

* Don't abuse the system property supported_features anymore.

* Check the correct method.

* Refactoring.

* Make hound happy.

* pythion-miio version bumped.

* Clean-up.

* Unique id added.

* Filter service calls.
Device unavailable handling improved.
2018-03-16 19:58:03 +01:00
Teemu R
78144bc6de Use the first, not the last volume controller when multiple are available on songpal (#13222)
* use the first, not the last volume controller

* Do not mutate the list but simply pick the first by index
2018-03-16 12:14:21 +01:00
cdce8p
f6ae2d338d Homekit: Use util functions for unit conversion (#13253)
* Updated to util/color for conversion
* Updated temperature sensor to use util/temperature conversion
2018-03-16 11:38:44 +01:00
BioSehnsucht
99f7e2bd97 Added Stride notification component (#13221)
* Added Stride notification component

* Fix trailing whitespace in Stride notify

* More whitespace fixes and rogue comment for Stride notify

* More whitespace fixing for Stride notify

* Correcting hanging indents for Stride notify
2018-03-15 20:36:03 -07:00
karlkar
b1079cb493 Fix for not setting up the camera if it is offline during setup phase (#13082)
* Fix for not setting up the camera if it is offline during setup phase

* async/await and modified service creation

* Properly handle not supported PTZ

* setup platform made synchronous as ONVIFService constructors do I/O

* Fix intendation issue
2018-03-15 20:30:41 -07:00
Fabien Piuzzi
0deef34881 Adding Foobot device sensor (#12417)
* Added Foobot device sensor

* Added error handling tests

* Corrections after PR review.

* Migrated to async/await syntax

 * lint fixes
 * stop raising HomeAssistantError
 * debug log for number of requests

* Moved shared data between sensors from a class attribute to a separate class

* Made test more async-aware

disabled setup error test for now as it's not working

* Working failure scenarios tests
2018-03-15 19:50:58 -07:00
cdce8p
2350ce96a6 Homekit: New supported devices (#13244)
* Fixed log message
* Added support for scripts
* Added support for lights
* Small refactoring
* Added support for humidity sensor
* Added tests
2018-03-16 01:05:28 +01:00
Anders Melchiorsen
de1ff1e952 Fix Sonos join/unjoin in scripts (#13248) 2018-03-15 16:12:43 -07:00
Gerard
d13bcf8412 Add extra sensors for BMW ConnectedDrive (#12591)
* Added extra sensors for BMW ConnectedDrive

* Updates based on review of @MartinHjelmare

* Updates based on 2nd review of @MartinHjelmare

* Changed control flow for updates to support updates triggered by remote services.

* updated library version number

* Changed order of commands so that the UI looks consistent.

State of lock is now set optimisitcally before getting proper update
from the server. So that the state does not toggle in the UI.

* Added comment on optimistic state

* Updated requirements_all.txt

* Revert access permission changes

* Fix for Travis

* Changes based on review by @MartinHjelmare
2018-03-15 22:56:35 +01:00
Paulus Schoutsen
456ff4e84b Tado: don't reference unset hass var (#13237)
Tado: don't reference unset hass var
2018-03-15 13:53:59 -07:00
Paulus Schoutsen
89a19c89a7 Fix aiohttp deprecation warnings (#13240)
* Fix aiohttp deprecation warnings

* Fix Ring deprecation warning

* Lint
2018-03-15 21:49:49 +01:00
Juggels
a86bf81768 Fix 'dict' object has no attribute 'strftime' (#13215)
* Fix 'dict' object has no attribute 'strftime'

* Clear existing list instead of new object
2018-03-15 21:43:20 +01:00
Anders Melchiorsen
5e675677ad Cleanup Sonos platform setup (#13225)
* Cleanup Sonos platform setup

* Remove unneeded lists
2018-03-15 20:43:28 +01:00
maxlaverse
ff416c0e7a Try to fix caldav (#13236)
* Fix device attribute type for event end

* Fix is_over and add tests
2018-03-15 10:58:11 -07:00
Paulus Schoutsen
170b8671b9 Fix logbook JSON serialize issue (#13229)
* Fix logbook JSON serialize issue

* Address flakiness

* Lint

* deflake ?

* Deflake 2
2018-03-15 10:54:22 -07:00
Maximilien Cuony
ee6d6a8859 myStrom: Add RGB support to Wifi bulbs (#13194) 2018-03-15 16:45:27 +01:00
Andrei Pop
1d2fd8a2e9 Edimax component reports wrong power values (#13011)
* Fixed Edimax switch authentication error for newer firmware.

* pyedimax moved to pypi

* Added pyedimax to gen_requirements_all.py

* Cleanup

* Fixed https://github.com/home-assistant/home-assistant/issues/13008

* Only ValueError now

* Trivial error.
2018-03-15 16:27:42 +01:00
Eugene Kuzin
92f13ff60d media_content_type attribute display fix (#13204)
* media_content_type fix

Kodi media_content_type attribute display fix

* media_content_type fix (#6989)

fixes attribute display for unknown media

* code cleanup

* trailing whitespaces

* comments correction

* redundant "else:" removed
2018-03-15 08:43:29 -04:00
Clement Wong
5c434f143e Tibber use appNickname as name (#13231) 2018-03-15 13:16:52 +01:00
cdce8p
646ed5de52 Added cover.group platform (replaces #12303) (#12692)
* Added cover.group platform
* Added async/await, smaller changes
* Made (async_update) methods regular methods
* Small improvements
* Changed classname
* Changes based on feedback
* Service calls
* update_supported_features is now a callback method
* combined all 'update_attr_*' methods in 'async_update'
* Small changes
* Fixes
   * is_closed
   * current_position
   * current_tilt_position
* Updated tests
* Small changes 2
2018-03-15 12:31:31 +01:00
Otto Winter
27c1806897 Python 3.5 adjustments (#13173) 2018-03-15 13:10:54 +02:00
Fabian Affolter
6909be1cc7 Add docstring (#13232) 2018-03-15 11:45:54 +01:00
PhracturedBlue
223bc187dc More robust MJPEG parser. Fixes #13138. (#13226)
* More robust MJPEG parser. Fixes ##13138.

* Reimplement image extraction from mjpeg without ascy generator to support python 3.5
2018-03-14 21:44:13 -07:00
c727
c971d61422 Change Hass.io icon to home-assistant (#13230) 2018-03-14 20:56:56 -07:00
Kane610
e122692b46 deCONZ - Add support for consumption and power sensors (#13218)
* Add support for consumption and power sensors

* Keep attr_current inside component
2018-03-14 20:07:37 -07:00
Paulus Schoutsen
76874e1cbc Update translations 2018-03-14 19:47:31 -07:00
cdce8p
d348f09d3d HomeKit Restructure (new config options) (#12997)
* Restructure
* Pincode will now be autogenerated and display using a persistence notification
* Added 'homekit.start' service
* Added config options
* Renamed files for types
* Improved tests
* Changes (based on feedback)
* Removed CONF_PIN_CODE
* Added services.yaml
* Service will only be registered if auto_start=False
* Bugfix names, changed default port
* Generate aids with zlib.adler32
* Added entity filter, minor changes
* Small changes
2018-03-15 02:48:21 +01:00
Paulus Schoutsen
64f18c62f4 Update frontend 2018-03-14 16:39:15 -07:00
Paulus Schoutsen
be2e202618 Bump frontend to 20180315.0 2018-03-14 16:13:46 -07:00
engrbm87
07f20676cb Add notifications to downloader.py (#12961)
* Update downloader.py

Add persistent notification to alert when download is finished or in case of download failure.

* Update downloader.py

* Update downloader.py

* Update downloader.py

* Fire and event when download is requested

Added 2 events to represent download completed and download failed. This will allow the user to trigger an automation based on the status of the download.

* Update downloader.py

* Update downloader.py

replaced . with _

* Update downloader.py

fixed linting errors
2018-03-14 16:03:40 -07:00
Paulus Schoutsen
a30ca4307b Merge branch 'master' into dev 2018-03-14 15:58:55 -07:00
Paulus Schoutsen
8d52eba484 Merge pull request #13223 from home-assistant/release-0-65-5
0.65.5
2018-03-14 15:55:50 -07:00
Paulus Schoutsen
8e05a5c12b Version bump to 0.65.6 2018-03-14 15:08:34 -07:00
Paulus Schoutsen
25fe6ec536 Fix input_boolean Google Assistant serialize error (#13220) 2018-03-14 15:08:23 -07:00
Anders Melchiorsen
30a1fedce8 Avoid Sonos error when joining with self (#13196) 2018-03-14 15:08:23 -07:00
Anders Melchiorsen
4e569ac0c3 Ignore unsupported Sonos favorites (#13195) 2018-03-14 15:08:22 -07:00
Alok Saboo
8a6370f7c9 Revert throttle Arlo api calls (#13174) 2018-03-14 15:08:21 -07:00
cdce8p
874cccd530 Bugfix HomeKit: Error string values for temperature (#13162) 2018-03-14 15:08:21 -07:00
Paulus Schoutsen
e1a5e5a8ba Fix input_boolean Google Assistant serialize error (#13220) 2018-03-14 15:07:37 -07:00
Paulus Schoutsen
a9917e7a56 Fix history API (#13214) 2018-03-14 22:29:51 +01:00
Anders Melchiorsen
ef7ce5eb1b Ignore unsupported Sonos favorites (#13195) 2018-03-14 12:08:41 -07:00
Anders Melchiorsen
7fc9ac0931 Avoid Sonos error when joining with self (#13196) 2018-03-14 12:07:50 -07:00
Paulus Schoutsen
e2029e3970 Add vesync to coveragerc 2018-03-14 12:05:17 -07:00
Paulus Schoutsen
7e2fc19f5a Sort coveragerc 2018-03-14 11:39:38 -07:00
cdce8p
c48c8710b7 Bugfix HomeKit: Error string values for temperature (#13162) 2018-03-14 13:22:38 +01:00
Dan Nixon
b6bed1dfab Report swap in MiB (#13148)
It makes sense to report swap and memory in the same unit and MiB is
more useful considering Home Assistant may be running on lower end
hardware (Raspberry Pi for example) where 100MiB resolution is not
adequate.
2018-03-14 08:47:45 +01:00
Vincent Van Den Berghe
948f29544a Fixed SI units for current consumption (#13190) 2018-03-14 08:14:35 +01:00
Mark Perdue
6310deb5c2 Add new platform for VeSync switches (#13000)
* Added vesync platform
Support for power toggling, current power, and daily energy kWh

* Adds vesync to requirements file.

* Reorder vesync item in requirements_all.txt from gen_requirements_all

* Removes unnecessary global values that are not used in this component

* Removes try/catch from setup_platform -no throws. Guard check login()

* Remove unnecessary boolean convert

* Fix indentation of log messages
2018-03-14 00:10:47 -07:00
JC Connell
cfded7eab9 Python Spotcrime sensor requires API key, fixes include/exclude (#12926)
* Add spotcrime.py to dev

* Modify sensor to accept user API key

* Update Spotcrime to 1.0.3 in requirements_all.txt

* Fix line 76 (97 > 79 characters)

* Fix lint errors
2018-03-14 08:01:10 +01:00
Pascal Vizeli
0ef4340099 Fix freegeoip (#13193) 2018-03-13 20:50:08 -07:00
Jon Maddox
24a9da85c0 Channels clean ups (#12967)
* already in the default schema

* these are already globally disabled

* require a single entity id

* remove unused import

* w h i t e s p a c e

* actually keep it

* it is a string

* use a generator expression

* 💄

* Revert "💄"

This reverts commit 81c08bb732.

* Revert "actually keep it"

This reverts commit 0d92d3afb2.

* Revert "remove unused import"

This reverts commit 8a166208e4.

* Revert "already in the default schema"

This reverts commit 9173de4fd3.

* we're already ensuring defaults with the platform schema
2018-03-13 14:14:02 -07:00
Alok Saboo
71baa6532e Revert throttle Arlo api calls (#13174) 2018-03-13 14:12:28 -07:00
Otto Winter
4c9e7c2da4 Upgrade pytest to 3.4.2 (#13169)
* Upgrade pytest to 3.4.2

* Upgrade pytest-sugar to 0.9.1
2018-03-13 13:57:04 -07:00
Otto Winter
5958e6a60f Improve MQTT failed connection error message (#13184) 2018-03-13 13:56:16 -07:00
Andreas Wolter
3e7a737bff Added IPAreaThermostat and an exception-list for HM_IGNORE_DISCOVERY_NODE (#13171) 2018-03-13 21:54:09 +01:00
Ville Skyttä
dd48fb04a3 upcloud: Provide unique ID for server entities (#13181) 2018-03-13 13:51:10 -07:00
Otto Winter
d5612b5ccc Upgrade holidays to 0.9.4 (#13167) 2018-03-13 17:30:31 +01:00
Kane610
8a1687accb deConz rewrite to use async await syntax (#13151)
* Rewrite to use async await syntax

* Fix hound comment
2018-03-13 08:47:45 +01:00
Jesse Hills
53351423dd Change iglo port to cv.port validator (#13163) 2018-03-13 08:29:20 +01:00
Daniel Høyer Iversen
75fb8ef98b upgrade tibber libary to 0.4.0 to use aiohttp 3.0 (#13164) 2018-03-13 07:04:27 +01:00
Fabian Affolter
989638b266 Upgrade Sphinx to 1.7.1 (#13127) 2018-03-12 18:22:48 -07:00
Otto Winter
d028c33e7f Disable Monkey Patch for 3.6.3+ (#13150) 2018-03-12 16:12:21 -07:00
Paulus Schoutsen
0a2e949e0a Remove crazy JSON encoding things that are no longer used (#13029)
Catch JSON encoding errors in HTTP view
2018-03-12 23:22:08 +01:00
Paulus Schoutsen
1bea8747ac Merge pull request #13149 from home-assistant/release-0-65-4
0.65.4
2018-03-12 14:52:01 -07:00
Alok Saboo
e54394e906 Throttle Arlo api calls (#13143) 2018-03-12 14:14:43 -07:00
Jeroen ter Heerdt
c384fd9653 Adding check for empty discovery info in alarm control panel Egardia. (#13114) 2018-03-12 14:14:42 -07:00
Paulus Schoutsen
3560fa754c Catch if bridge goes unavailable (#13109) 2018-03-12 14:14:42 -07:00
Paulus Schoutsen
101a6ab07c Fix unavailable property for wemo switch (#13106)
* Fix unavailable property for wemo switch

* Have subscriptions respect the lock

* Move subscription callback to added to hass section
2018-03-12 14:14:42 -07:00
Paulus Schoutsen
eb1ca20cfc Version bump to 0.65.4 2018-03-12 14:14:36 -07:00
Jeroen ter Heerdt
ae286a550b Adding check for empty discovery info in alarm control panel Egardia. (#13114) 2018-03-12 14:03:05 -07:00
Fabian Affolter
c5330a13b6 Upgrade schiene to 0.22 (#13121) 2018-03-12 14:02:36 -07:00
Fabian Affolter
6ab4a408d2 Upgrade zeroconf to 0.20.0 (#13123) 2018-03-12 14:02:03 -07:00
Fabian Affolter
1202134964 Upgrade youtube_dl to 2018.03.10 (#13124) 2018-03-12 14:01:32 -07:00
Fabian Affolter
bbbb44b999 Upgrade TwitterAPI to 2.4.10 (#13126) 2018-03-12 14:01:05 -07:00
Fabian Affolter
54e0cc1304 Upgrade mypy to 0.570 (#13128) 2018-03-12 14:00:10 -07:00
Per Osbäck
f9e07e617c update to async/await (#13137) 2018-03-12 13:57:13 -07:00
Alok Saboo
95a528a75f Throttle Arlo api calls (#13143) 2018-03-12 13:56:33 -07:00
Paulus Schoutsen
51b0cbefe3 Catch if bridge goes unavailable (#13109) 2018-03-12 13:55:22 -07:00
Paulus Schoutsen
8d8b07abd5 Fix unavailable property for wemo switch (#13106)
* Fix unavailable property for wemo switch

* Have subscriptions respect the lock

* Move subscription callback to added to hass section
2018-03-12 13:54:56 -07:00
Federico Zivolo
15d345c4ef fix: Support different JointSpace API versions (#13084) 2018-03-12 12:33:04 -07:00
Fabian Affolter
676c94561b Upgrade astral to 1.6 (#13120) 2018-03-12 12:28:27 -07:00
Fabian Affolter
02ad9c3574 Upgrade aiohttp to 3.0.7 (#13119) 2018-03-12 12:26:51 -07:00
Otto Winter
890197e407 asyncio.ensure_future Python 3.5 (#13141) 2018-03-12 19:42:08 +01:00
Paulus Schoutsen
9ee123f5ce Version bump to 0.66.0.dev0 2018-03-11 13:20:47 -07:00
Adam Mills
14aa4e7694 Lint script tweaks (#13093)
* Also lint working tree files

When performing a git diff of upstream/dev..., git is diffing against
the current HEAD, but does not include working tree files. By manually
calculating a merge-base SHA to diff against, git will still diff those
files.

* Don't pylint tests files, since we don't in CI

* Use merge base for lazytox

* Simplify files changed header
2018-03-11 16:15:09 -04:00
tadly
991c457430 Updated jsonrpc-websocket to 0.6 (#13096)
Fix Kodi by updating jsonrpc-websocket to 0.6
2018-03-11 12:46:16 -07:00
Greg Dowling
401e92f84e Bump pyvera to 0.2.42. Improve event loop robustness. (#13095) 2018-03-11 12:43:28 -07:00
Otto Winter
1dc5fa145f Revert "Cast automatically drop connection (#12635)" (#13094)
This reverts commit e14893416f.
2018-03-11 12:42:58 -07:00
Joe Lu
dff4f6ce48 Integrated with py-synology:0.2.0 which has fix to auto-renew session when it's expired (#13079) 2018-03-11 12:33:36 -07:00
Paulus Schoutsen
56b3cb0583 Fix Tado doing async wrong (#13078)
* Fix Tado doing async wrong

* Remove last coroutine decorator
2018-03-11 12:33:07 -07:00
Paulus Schoutsen
d0f089975d Run asyncio event loop in debug mode during tests (#13058)
* Run asyncio event loop in debug mode during tests

* Remove debug mode again
2018-03-11 12:32:12 -07:00
Adam Mills
26960283a0 Config flow translations (#13066)
* Development script for testing translation strings

* Localize backend of config flow

* Fix hue tests

* Update hue.en.json

* Move components to individual directories

* Bridge -> bridge
2018-03-11 12:04:34 -07:00
Matthias Urlichs
f5cc40024d Rename homeassistant.util.async to .async_ (#13088)
"async" is (going to be) a reserved word.
2018-03-11 10:01:12 -07:00
Paulus Schoutsen
d42b5a93dd Implement Hue available property (#12939) 2018-03-11 09:49:28 -07:00
Andrey
3dfc49d311 Make Sensibo climate registry_entity compliant (#13086) 2018-03-11 17:19:25 +01:00
Adam Mills
dc8424032b Remove Z-Wave old/new entity_id attributes (#12652) 2018-03-11 10:30:03 -04:00
Johann Kellerman
f164a5a65f Better errors for unknown secrets (#13072) 2018-03-11 12:51:03 +02:00
Otto Winter
d74a2b68c1 Sensor template don't exit early on TemplateError (#13041)
* Sensor template don't exit early on TemplateError

* Add friendly name unknown state test

* Also track entites from attribute templates

* Use set instead of list
2018-03-10 20:45:32 -08:00
Jesse Hills
458598546d - Bump iGlo Version (#13063)
- Use effect list as a method
2018-03-10 20:31:57 -08:00
Julius Mittenzwei
3f6d30ed06 Fixes KNX fire event problem, issue https://github.com/home-assistant/home-assistant/issues/13049 (#13062) 2018-03-10 20:26:21 -08:00
Diogo Gomes
28ff1f7ac2 Convert decimals from SQL results 2018-03-11 00:27:58 +02:00
Paulus Schoutsen
0e00de8a33 Convert decimals from SQL results (#13059) 2018-03-10 10:40:28 -08:00
Paulus Schoutsen
ae47da7bce Update frontend to 20180310.0 2018-03-10 10:19:49 -08:00
Paulus Schoutsen
f01b5b0040 Don't call async from sync (#13057) 2018-03-10 10:02:16 -08:00
Johann Kellerman
40485a6e89 Ensure we have valid config AFTER merging packages #13015 (#13038)
* Ensure we have valid config AFTER merging packages #13015

* also fix packages
2018-03-10 10:02:04 -08:00
cdce8p
7ea7fc8d38 Script/lint, Lazytox: Fix issue to ignore delete files (#13051)
* Fix issue to ignore delete files

* Updated lazytox
2018-03-10 09:12:23 -08:00
Timmo
86baed4e52 Glances Docker Sensors (#13026)
* Added container count

* Added container count

* Change Name

* Fix if

* Added Docker cpu use and memory use
2018-03-10 18:11:53 +01:00
Sebastian Muszynski
b4b779c49d python-miio version bumped. (#13055) 2018-03-10 09:11:10 -08:00
Sebastian Muszynski
0143752d94 Yeelight version bumped. (#13056) 2018-03-10 09:10:50 -08:00
Anders Melchiorsen
e910ecfd5f Fix async lifx_set_state (#13045) 2018-03-10 11:07:02 +01:00
John Allen
4d74fc2d07 Fix sensibo's min/max_temp properties (#12996)
The super class has these as properties, not regular methods
2018-03-10 10:52:45 +02:00
Paul Tarjan
f9c1675c95 Use request.query (#13037)
Fixes #13036
2018-03-10 00:36:20 -08:00
Jerad Meisner
76fb2447a5 Bump pysabnzbd version (#13042) 2018-03-10 09:27:13 +01:00
Otto Winter
2fae86bbd3 Make lazytox script executable (#13040) 2018-03-10 08:39:49 +01:00
cdce8p
3ca139e21e HomeKit Bugfix: names (#13031)
* Fix display_names, changed default port (+1)

* Revert port change
2018-03-09 19:41:59 -08:00
Paulus Schoutsen
a8a895a61b allow ios device tracker see calls to go through (#13020) 2018-03-09 19:39:50 -08:00
Paulus Schoutsen
36361d623d Make Throttle async aware (#13027)
* Make Throttle async aware

* Lint
2018-03-09 19:38:51 -08:00
Paulus Schoutsen
652e0d45a9 Add support for input boolean to Google Assistant (#13030) 2018-03-09 19:38:33 -08:00
Heiko Thiery
556901ea48 remove rounding of temperature reading (#13018)
With homeassistant 0.65.0 the filter sensor is introduced. Now there
is a common way to filter the peaks comming from the readings.

Signed-off-by: Heiko Thiery <heiko.thiery@gmail.com>
2018-03-09 15:53:28 -08:00
Johann Kellerman
05255b9c3f Safe fix for #13015 (#13024) 2018-03-09 15:52:21 -08:00
Paulus Schoutsen
11e1b8a19d Update netdisco to 1.3.0 (#13007) 2018-03-09 13:04:23 -08:00
Johann Kellerman
37d8cd7b75 New lazytox.py script (#12862) 2018-03-09 22:27:39 +02:00
Ryan McLean
d8a7c547df Updated to plexapi 3.0.6 (#13005) 2018-03-09 08:50:39 -08:00
Ryan McLean
ecaf0189cc Plex mark devices unavailable if they 'vanish' and clear media (#12811)
* Marks Devices unavailable if they 'vanish' and clears media

* Fixed PEP8 complaint

* Fixed Linting

* Lint Fix

* Fix redine of id

* More lint fixes

* Removed redundant loop for setting availability of client
Renamed '_is_device_available' to '_available'
Renamed 'available_ids' to 'available_client_ids'

* removed whitespace per houndCI
2018-03-09 08:50:21 -08:00
Otto Winter
ca5f470956 Clean up Light Groups (#12962)
* Clean up Light Groups

* Fix tests

* Remove light group from .coveragerc

* async_schedule_update_ha_state called anyway
2018-03-09 06:15:39 -08:00
mueslo
3ba19c52d5 Add consider_home and source_type to device_tracker.see service (#12849)
* Add consider_home and source_type to device_tracker.see service

* Use schema instead of manual validation

* Extend schema to validate all keys

* Fix style

* Set battery level to int
2018-03-08 23:57:21 -08:00
Johann Kellerman
6734c966b3 check_config script evolution (#12792)
* Initial async_check_ha_config_file

* check_ha_config_file

* Various fixes

* feedback - return the config

* move_to_check_config
2018-03-08 19:34:24 -08:00
Steve Easley
5e2296f2a4 Get zha switch and binary_sensor state on startup (#11672)
* Get zha switch and binary_sensor state on startup

* Removed unused var

* Make zha switch report status

* Use right method name

* Formatting fix

* Updates to match latest dev

* PR feedback updates

* Use async for cluster commands
2018-03-08 19:31:52 -08:00
PhracturedBlue
c5228cbc64 Add camera proxy (#12006)
* Add camera proxy

* Fix additional tox linting issues

* Trivial cleanup

* update to new async/await methods rather than decorators.  Other minor fixes from code review
2018-03-08 18:23:52 -08:00
475 changed files with 15789 additions and 7215 deletions

View File

@@ -109,6 +109,9 @@ omit =
homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py
homeassistant/components/homematicip_cloud.py
homeassistant/components/*/homematicip_cloud.py
homeassistant/components/ihc/*
homeassistant/components/*/ihc.py
@@ -157,9 +160,6 @@ omit =
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/mercedesme.py
homeassistant/components/*/mercedesme.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
@@ -286,11 +286,9 @@ omit =
homeassistant/components/*/wink.py
homeassistant/components/xiaomi_aqara.py
homeassistant/components/binary_sensor/xiaomi_aqara.py
homeassistant/components/cover/xiaomi_aqara.py
homeassistant/components/light/xiaomi_aqara.py
homeassistant/components/sensor/xiaomi_aqara.py
homeassistant/components/switch/xiaomi_aqara.py
homeassistant/components/*/xiaomi_aqara.py
homeassistant/components/*/xiaomi_miio.py
homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py
@@ -309,6 +307,7 @@ omit =
homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/ialarm.py
homeassistant/components/alarm_control_panel/ifttt.py
homeassistant/components/alarm_control_panel/manual_mqtt.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
@@ -332,6 +331,7 @@ omit =
homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/onvif.py
homeassistant/components/camera/proxy.py
homeassistant/components/camera/ring.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py
@@ -352,6 +352,7 @@ omit =
homeassistant/components/climate/touchline.py
homeassistant/components/climate/venstar.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/gogogate2.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py
homeassistant/components/cover/myq.py
@@ -369,6 +370,7 @@ omit =
homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py
@@ -395,28 +397,29 @@ omit =
homeassistant/components/emoncms_history.py
homeassistant/components/emulated_hue/upnp.py
homeassistant/components/fan/mqtt.py
homeassistant/components/fan/xiaomi_miio.py
homeassistant/components/feedreader.py
homeassistant/components/folder_watcher.py
homeassistant/components/foursquare.py
homeassistant/components/goalfeed.py
homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py
homeassistant/components/keyboard.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
homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py
homeassistant/components/light/decora.py
homeassistant/components/light/flux_led.py
homeassistant/components/light/greenwave.py
homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/iglo.py
homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py
homeassistant/components/light/osramlightify.py
@@ -427,7 +430,6 @@ omit =
homeassistant/components/light/tplink.py
homeassistant/components/light/tradfri.py
homeassistant/components/light/x10.py
homeassistant/components/light/xiaomi_miio.py
homeassistant/components/light/yeelight.py
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py
@@ -436,12 +438,14 @@ omit =
homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py
homeassistant/components/lock/sesame.py
homeassistant/components/map.py
homeassistant/components/media_extractor.py
homeassistant/components/media_player/anthemav.py
homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/bluesound.py
homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/channels.py
homeassistant/components/media_player/clementine.py
homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py
@@ -482,8 +486,8 @@ omit =
homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py
homeassistant/components/media_player/xiaomi_tv.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/yamaha_musiccast.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/ziggo_mediabox_xl.py
homeassistant/components/mycroft.py
homeassistant/components/notify/aws_lambda.py
@@ -491,8 +495,8 @@ omit =
homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/ciscospark.py
homeassistant/components/notify/clickatell.py
homeassistant/components/notify/clicksend.py
homeassistant/components/notify/clicksend_tts.py
homeassistant/components/notify/clicksend.py
homeassistant/components/notify/discord.py
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py
@@ -502,6 +506,7 @@ omit =
homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/mastodon.py
homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py
@@ -518,6 +523,7 @@ omit =
homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py
homeassistant/components/notify/stride.py
homeassistant/components/notify/synology_chat.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py
@@ -531,7 +537,6 @@ omit =
homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py
homeassistant/components/remote/xiaomi_miio.py
homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/airvisual.py
@@ -554,7 +559,6 @@ omit =
homeassistant/components/sensor/crimereports.py
homeassistant/components/sensor/cups.py
homeassistant/components/sensor/currencylayer.py
homeassistant/components/sensor/darksky.py
homeassistant/components/sensor/deluge.py
homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py
@@ -576,6 +580,7 @@ omit =
homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/folder.py
homeassistant/components/sensor/foobot.py
homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/gearbest.py
@@ -588,8 +593,8 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/influxdb.py
homeassistant/components/sensor/irish_rail_transport.py
homeassistant/components/sensor/kwb.py
@@ -632,8 +637,8 @@ omit =
homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sense.py
homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/simulated.py
homeassistant/components/sensor/skybeacon.py
@@ -647,6 +652,7 @@ omit =
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/syncthru.py
homeassistant/components/sensor/synologydsm.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/sytadin.py
@@ -656,6 +662,7 @@ omit =
homeassistant/components/sensor/tibber.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py
homeassistant/components/sensor/trafikverket_weatherstation.py
homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/travisci.py
homeassistant/components/sensor/twitch.py
@@ -665,6 +672,7 @@ omit =
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/waze_travel_time.py
homeassistant/components/sensor/whois.py
homeassistant/components/sensor/worldtidesinfo.py
homeassistant/components/sensor/worxlandroid.py
@@ -697,7 +705,7 @@ omit =
homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/xiaomi_miio.py
homeassistant/components/switch/vesync.py
homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py
@@ -706,7 +714,6 @@ omit =
homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py
homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py

View File

@@ -1,35 +1,45 @@
Make sure you are running the latest version of Home Assistant before reporting an issue.
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
-->
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
**Home Assistant release (`hass --version`):**
**Home Assistant release with the issue:**
<!--
- Frontend -> Developer tools -> Info
- Or use this command: hass --version
-->
**Python release (`python3 --version`):**
**Last working Home Assistant release (if known):**
**Operating environment (Hass.io/Docker/Windows/etc.):**
<!--
Please provide details about your environment.
-->
**Component/platform:**
<!--
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
-->
**Description of problem:**
**Expected:**
**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
```yaml
```
1.
2.
3.
**Traceback (if applicable):**
```bash
```
```
**Additional info:**
**Additional information:**

View File

@@ -12,19 +12,18 @@
## Checklist:
- [ ] The code change is tested and works locally.
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
If the code communicates with devices, web services, or third-party tools:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
- [ ] New files were added to `.coveragerc`.
If the code does not interact with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] Tests have been added to verify that the new code works.
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14

1
.gitignore vendored
View File

@@ -21,6 +21,7 @@ Icon
*.iml
# pytest
.pytest_cache
.cache
# GITHUB Proposed Python stuff:

0
.gitmodules vendored
View File

View File

@@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core
homeassistant/components/websocket_api.py @home-assistant/core
homeassistant/components/zone.py @home-assistant/core
# To monitor non-pypi additions
requirements_all.txt @andrey-git
# HomeAssistant developer Teams
Dockerfile @home-assistant/docker
virtualization/Docker/* @home-assistant/docker
@@ -43,12 +40,14 @@ homeassistant/components/hassio.py @home-assistant/hassio
# Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50
homeassistant/components/climate/eq3btsmart.py @rytilahti
homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/tile.py @bachya
@@ -68,8 +67,10 @@ homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/qnap.py @colinodell
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/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
@@ -79,17 +80,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610
homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p
homeassistant/components/*/deconz.py @kane610
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342
homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tesla.py @zabuldon
@@ -97,5 +98,9 @@ homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
homeassistant/scripts/check_config.py @kellerza

View File

@@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing.
- Ensure tests work.

View File

@@ -4,10 +4,10 @@ homeassistant.util package
Submodules
----------
homeassistant.util.async module
homeassistant.util.async_ module
-------------------------------
.. automodule:: homeassistant.util.async
.. automodule:: homeassistant.util.async_
:members:
:undoc-members:
:show-inheritance:

View File

@@ -272,7 +272,7 @@ def setup_and_run_hass(config_dir: str,
if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch
from homeassistant.util.async import run_callback_threadsafe
from homeassistant.util.async_ import run_callback_threadsafe
def open_browser(event):
"""Open the webinterface in a browser."""
@@ -335,7 +335,8 @@ def main() -> int:
"""Start Home Assistant."""
validate_python()
if os.environ.get('HASS_NO_MONKEY') != '1':
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1':
if sys.version_info[:2] >= (3, 6):
monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks()

View File

@@ -86,14 +86,6 @@ def async_from_config_dict(config: Dict[str, Any],
if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file)
if sys.version_info[:2] < (3, 5):
_LOGGER.warning(
'Python 3.4 support has been deprecated and will be removed in '
'the beginning of 2018. Please upgrade Python or your operating '
'system. More info: https://home-assistant.io/blog/2017/10/06/'
'deprecating-python-3.4-support/'
)
core_config = config.get(core.DOMAIN, {})
try:

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.2']
REQUIREMENTS = ['abodepy==0.12.3']
_LOGGER = logging.getLogger(__name__)

View File

@@ -12,13 +12,14 @@ import requests
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED,
STATE_ALARM_ARMED_NIGHT)
from homeassistant.components.egardia import (
EGARDIA_DEVICE, EGARDIA_SERVER,
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
)
REQUIREMENTS = ['pythonegardia==1.0.38']
DEPENDENCIES = ['egardia']
_LOGGER = logging.getLogger(__name__)
@@ -27,12 +28,16 @@ STATES = {
'DAY HOME': STATE_ALARM_ARMED_HOME,
'DISARM': STATE_ALARM_DISARMED,
'ARMHOME': STATE_ALARM_ARMED_HOME,
'HOME': STATE_ALARM_ARMED_HOME,
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
'TRIGGERED': STATE_ALARM_TRIGGERED
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Egardia platform."""
if discovery_info is None:
return
device = EgardiaAlarm(
discovery_info['name'],
hass.data[EGARDIA_DEVICE],

View File

@@ -0,0 +1,170 @@
"""
Interfaces with alarm control panels that have to be controlled through IFTTT.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.ifttt/
"""
import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import (
DOMAIN, PLATFORM_SCHEMA)
from homeassistant.components.ifttt import (
ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE,
CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['ifttt']
_LOGGER = logging.getLogger(__name__)
ALLOWED_STATES = [
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME]
DATA_IFTTT_ALARM = 'ifttt_alarm'
DEFAULT_NAME = "Home"
CONF_EVENT_AWAY = "event_arm_away"
CONF_EVENT_HOME = "event_arm_home"
CONF_EVENT_NIGHT = "event_arm_night"
CONF_EVENT_DISARM = "event_disarm"
DEFAULT_EVENT_AWAY = "alarm_arm_away"
DEFAULT_EVENT_HOME = "alarm_arm_home"
DEFAULT_EVENT_NIGHT = "alarm_arm_night"
DEFAULT_EVENT_DISARM = "alarm_disarm"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string,
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
})
SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state"
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_STATE): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a control panel managed through IFTTT."""
if DATA_IFTTT_ALARM not in hass.data:
hass.data[DATA_IFTTT_ALARM] = []
name = config.get(CONF_NAME)
code = config.get(CONF_CODE)
event_away = config.get(CONF_EVENT_AWAY)
event_home = config.get(CONF_EVENT_HOME)
event_night = config.get(CONF_EVENT_NIGHT)
event_disarm = config.get(CONF_EVENT_DISARM)
optimistic = config.get(CONF_OPTIMISTIC)
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
event_night, event_disarm, optimistic)
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
add_devices([alarmpanel])
async def push_state_update(service):
"""Set the service state as device state attribute."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
state = service.data.get(ATTR_STATE)
devices = hass.data[DATA_IFTTT_ALARM]
if entity_ids:
devices = [d for d in devices if d.entity_id in entity_ids]
for device in devices:
device.push_alarm_state(state)
device.async_schedule_update_ha_state()
hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update,
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA)
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an alarm control panel controlled through IFTTT."""
def __init__(self, name, code, event_away, event_home, event_night,
event_disarm, optimistic):
"""Initialize the alarm control panel."""
self._name = name
self._code = code
self._event_away = event_away
self._event_home = event_home
self._event_night = event_night
self._event_disarm = event_disarm
self._optimistic = optimistic
self._state = None
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def assumed_state(self):
"""Notify that this platform return an assumed state."""
return True
@property
def code_format(self):
"""Return one or more characters."""
return None if self._code is None else '.+'
def alarm_disarm(self, code=None):
"""Send disarm command."""
if not self._check_code(code):
return
self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if not self._check_code(code):
return
self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if not self._check_code(code):
return
self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
if not self._check_code(code):
return
self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT)
def set_alarm_state(self, event, state):
"""Call the IFTTT trigger service to change the alarm state."""
data = {ATTR_EVENT: event}
self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data)
_LOGGER.debug("Called IFTTT component to trigger event %s", event)
if self._optimistic:
self._state = state
def push_alarm_state(self, value):
"""Push the alarm state to the given value."""
if value in ALLOWED_STATES:
_LOGGER.debug("Pushed the alarm state to %s", value)
self._state = value
def _check_code(self, code):
return self._code is None or self._code == code

View File

@@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime:
code:
description: A required code to toggle the alarm control panel chime with.
example: 1234
ifttt_push_alarm_state:
description: Update the alarm state to the specified value.
fields:
entity_id:
description: Name of the alarm control panel which state has to be updated.
example: 'alarm_control_panel.downstairs'
state:
description: The state to which the alarm control panel has to be set.
example: 'armed_night'

View File

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

View File

@@ -6,18 +6,20 @@ from datetime import datetime
from uuid import uuid4
from homeassistant.components import (
alert, automation, cover, fan, group, input_boolean, light, lock,
alert, automation, cover, climate, fan, group, input_boolean, light, lock,
media_player, scene, script, switch, http, sensor)
import homeassistant.core as ha
import homeassistant.util.color as color_util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.util.decorator import Registry
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
_LOGGER = logging.getLogger(__name__)
@@ -34,6 +36,16 @@ API_TEMP_UNITS = {
TEMP_CELSIUS: 'CELSIUS',
}
API_THERMOSTAT_MODES = {
climate.STATE_HEAT: 'HEAT',
climate.STATE_COOL: 'COOL',
climate.STATE_AUTO: 'AUTO',
climate.STATE_ECO: 'ECO',
climate.STATE_IDLE: 'OFF',
climate.STATE_FAN_ONLY: 'OFF',
climate.STATE_DRY: 'OFF',
}
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
CONF_DESCRIPTION = 'description'
@@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
raise _UnsupportedProperty(name)
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = self.entity.state
if self.entity.domain == climate.DOMAIN:
temp = self.entity.attributes.get(
climate.ATTR_CURRENT_TEMPERATURE)
return {
'value': float(self.entity.state),
'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
class _AlexaThermostatController(_AlexaInterface):
def name(self):
return 'Alexa.ThermostatController'
def properties_supported(self):
properties = []
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
properties.append({'name': 'targetSetpoint'})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
properties.append({'name': 'lowerSetpoint'})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
properties.append({'name': 'upperSetpoint'})
if supported & climate.SUPPORT_OPERATION_MODE:
properties.append({'name': 'thermostatMode'})
return properties
def properties_retrievable(self):
return True
def get_property(self, name):
if name == 'thermostatMode':
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
mode = API_THERMOSTAT_MODES.get(ha_mode)
if mode is None:
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
self.entity.entity_id, type(self.entity),
climate.ATTR_OPERATION_MODE, ha_mode)
raise _UnsupportedProperty(name)
return mode
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = None
if name == 'targetSetpoint':
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
elif name == 'lowerSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
elif name == 'upperSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
if temp is None:
raise _UnsupportedProperty(name)
return {
'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
@@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
return [_AlexaPowerController(self.entity)]
@ENTITY_ADAPTERS.register(climate.DOMAIN)
class _ClimateCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.THERMOSTAT]
def interfaces(self):
yield _AlexaThermostatController(self.entity)
yield _AlexaTemperatureSensor(self.entity)
@ENTITY_ADAPTERS.register(cover.DOMAIN)
class _CoverCapabilities(_AlexaEntity):
def default_display_categories(self):
@@ -438,9 +512,7 @@ class _LightCapabilities(_AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & light.SUPPORT_BRIGHTNESS:
yield _AlexaBrightnessController(self.entity)
if supported & light.SUPPORT_RGB_COLOR:
yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_XY_COLOR:
if supported & light.SUPPORT_COLOR:
yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_COLOR_TEMP:
yield _AlexaColorTemperatureController(self.entity)
@@ -684,17 +756,26 @@ def api_message(request,
return response
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
def api_error(request,
namespace='Alexa',
error_type='INTERNAL_ERROR',
error_message="",
payload=None):
"""Create a API formatted error response.
Async friendly.
"""
payload = {
'type': error_type,
'message': error_message,
}
payload = payload or {}
payload['type'] = error_type
payload['message'] = error_message
return api_message(request, name='ErrorResponse', payload=payload)
_LOGGER.info("Request %s/%s error %s: %s",
request[API_HEADER]['namespace'],
request[API_HEADER]['name'],
error_type, error_message)
return api_message(
request, name='ErrorResponse', namespace=namespace, payload=payload)
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
@@ -842,25 +923,16 @@ def async_api_adjust_brightness(hass, config, request, entity):
@asyncio.coroutine
def async_api_set_color(hass, config, request, entity):
"""Process a set color request."""
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
rgb = color_util.color_hsb_to_RGB(
float(request[API_PAYLOAD]['color']['hue']),
float(request[API_PAYLOAD]['color']['saturation']),
float(request[API_PAYLOAD]['color']['brightness'])
)
if supported & light.SUPPORT_RGB_COLOR > 0:
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=False)
else:
xyz = color_util.color_RGB_to_xy(*rgb)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
light.ATTR_BRIGHTNESS: xyz[2],
}, blocking=False)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=False)
return api_message(request)
@@ -1115,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
else:
msg = 'failed to map input {} to a media source on {}'.format(
media_input, entity.entity_id)
_LOGGER.error(msg)
return api_error(
request, error_type='INVALID_VALUE', error_message=msg)
@@ -1287,6 +1358,149 @@ def async_api_previous(hass, config, request, entity):
return api_message(request)
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
"""Create temperature value out of range API error response.
Async friendly.
"""
temp_range = {
'minimumValue': {
'value': min_temp,
'scale': API_TEMP_UNITS[unit],
},
'maximumValue': {
'value': max_temp,
'scale': API_TEMP_UNITS[unit],
},
}
msg = 'The requested temperature {} is out of range'.format(temp)
return api_error(
request,
error_type='TEMPERATURE_VALUE_OUT_OF_RANGE',
error_message=msg,
payload={'validRange': temp_range},
)
def temperature_from_object(temp_obj, to_unit, interval=False):
"""Get temperature from Temperature object in requested unit."""
from_unit = TEMP_CELSIUS
temp = float(temp_obj['value'])
if temp_obj['scale'] == 'FAHRENHEIT':
from_unit = TEMP_FAHRENHEIT
elif temp_obj['scale'] == 'KELVIN':
# convert to Celsius if absolute temperature
if not interval:
temp -= 273.15
return convert_temperature(temp, from_unit, to_unit, interval)
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
@extract_entity
async def async_api_set_target_temp(hass, config, request, entity):
"""Process a set target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
data = {
ATTR_ENTITY_ID: entity.entity_id
}
payload = request[API_PAYLOAD]
if 'targetSetpoint' in payload:
temp = temperature_from_object(
payload['targetSetpoint'], unit)
if temp < min_temp or temp > max_temp:
return api_error_temp_range(
request, temp, min_temp, max_temp, unit)
data[ATTR_TEMPERATURE] = temp
if 'lowerSetpoint' in payload:
temp_low = temperature_from_object(
payload['lowerSetpoint'], unit)
if temp_low < min_temp or temp_low > max_temp:
return api_error_temp_range(
request, temp_low, min_temp, max_temp, unit)
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
if 'upperSetpoint' in payload:
temp_high = temperature_from_object(
payload['upperSetpoint'], unit)
if temp_high < min_temp or temp_high > max_temp:
return api_error_temp_range(
request, temp_high, min_temp, max_temp, unit)
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
@extract_entity
async def async_api_adjust_target_temp(hass, config, request, entity):
"""Process an adjust target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
temp_delta = temperature_from_object(
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
return api_error_temp_range(
request, target_temp, min_temp, max_temp, unit)
data = {
ATTR_ENTITY_ID: entity.entity_id,
ATTR_TEMPERATURE: target_temp,
}
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
@extract_entity
async def async_api_set_thermostat_mode(hass, config, request, entity):
"""Process a set thermostat mode request."""
mode = request[API_PAYLOAD]['thermostatMode']
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
# Work around a pylint false positive due to
# https://github.com/PyCQA/pylint/issues/1830
# pylint: disable=stop-iteration-return
ha_mode = next(
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
None
)
if ha_mode not in operation_list:
msg = 'The requested thermostat mode {} is not supported'.format(mode)
return api_error(
request,
namespace='Alexa.ThermostatController',
error_type='UNSUPPORTED_THERMOSTAT_MODE',
error_message=msg
)
data = {
ATTR_ENTITY_ID: entity.entity_id,
climate.ATTR_OPERATION_MODE: ha_mode,
}
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa', 'ReportState'))
@extract_entity
@asyncio.coroutine

View File

@@ -10,14 +10,15 @@ from datetime import timedelta
import aiohttp
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
from requests.exceptions import ConnectionError as ConnectError
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.1']
REQUIREMENTS = ['amcrest==1.2.2']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
@@ -63,6 +64,12 @@ SENSORS = {
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
}
# Switch types are defined like: Name, icon
SWITCHES = {
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
'motion_recording': ['Motion Recording', 'mdi:record-rec']
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_HOST): cv.string,
@@ -81,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({
cv.time_period,
vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
})])
}, extra=vol.ALLOW_EXTRA)
@@ -93,14 +102,15 @@ def setup(hass, config):
amcrest_cams = config[DOMAIN]
for device in amcrest_cams:
camera = AmcrestCamera(device.get(CONF_HOST),
device.get(CONF_PORT),
device.get(CONF_USERNAME),
device.get(CONF_PASSWORD)).camera
try:
camera = AmcrestCamera(device.get(CONF_HOST),
device.get(CONF_PORT),
device.get(CONF_USERNAME),
device.get(CONF_PASSWORD)).camera
# pylint: disable=pointless-statement
camera.current_time
except (ConnectTimeout, HTTPError) as ex:
except (ConnectError, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
@@ -108,12 +118,13 @@ def setup(hass, config):
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
continue
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
name = device.get(CONF_NAME)
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
sensors = device.get(CONF_SENSORS)
switches = device.get(CONF_SWITCHES)
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
username = device.get(CONF_USERNAME)
@@ -143,6 +154,13 @@ def setup(hass, config):
CONF_SENSORS: sensors,
}, config)
if switches:
discovery.load_platform(
hass, 'switch', DOMAIN, {
CONF_NAME: name,
CONF_SWITCHES: switches
}, config)
return True

View File

@@ -0,0 +1,119 @@
"""
Reads vehicle status from BMW connected drive portal.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
DEPENDENCIES = ['bmw_connected_drive']
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'lids': ['Doors', 'opening'],
'windows': ['Windows', 'opening'],
'door_lock_state': ['Door lock state', 'safety']
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the BMW sensors."""
accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s',
', '.join([a.name for a in accounts]))
devices = []
for account in accounts:
for vehicle in account.account.vehicles:
for key, value in sorted(SENSOR_TYPES.items()):
device = BMWConnectedDriveSensor(account, vehicle, key,
value[0], value[1])
devices.append(device)
add_devices(devices, True)
class BMWConnectedDriveSensor(BinarySensorDevice):
"""Representation of a BMW vehicle binary sensor."""
def __init__(self, account, vehicle, attribute: str, sensor_name,
device_class):
"""Constructor."""
self._account = account
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._sensor_name = sensor_name
self._device_class = device_class
self._state = None
@property
def should_poll(self) -> bool:
"""Data update is triggered from BMWConnectedDriveEntity."""
return False
@property
def name(self):
"""Return the name of the binary sensor."""
return self._name
@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state
result = {
'car': self._vehicle.name
}
if self._attribute == 'lids':
for lid in vehicle_state.lids:
result[lid.name] = lid.state.value
elif self._attribute == 'windows':
for window in vehicle_state.windows:
result[window.name] = window.state.value
elif self._attribute == 'door_lock_state':
result['door_lock_state'] = vehicle_state.door_lock_state.value
return result
def update(self):
"""Read new state data from the library."""
from bimmer_connected.state import LockState
vehicle_state = self._vehicle.state
# device class opening: On means open, Off means closed
if self._attribute == 'lids':
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
self._state = not vehicle_state.all_lids_closed
if self._attribute == 'windows':
self._state = not vehicle_state.all_windows_closed
# device class safety: On means unsafe, Off means safe
if self._attribute == 'door_lock_state':
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._state = vehicle_state.door_lock_state not in \
[LockState.LOCKED, LockState.SECURED]
def update_callback(self):
"""Schedule a state update."""
self.schedule_update_ha_state(True)
@asyncio.coroutine
def async_added_to_hass(self):
"""Add callback after being added to hass.
Show latest data after startup.
"""
self._account.add_update_listener(self.update_callback)

View File

@@ -4,8 +4,6 @@ Support for deCONZ binary sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.deconz/
"""
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
@@ -15,8 +13,8 @@ from homeassistant.core import callback
DEPENDENCIES = ['deconz']
@asyncio.coroutine
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 deCONZ binary sensor."""
if discovery_info is None:
return
@@ -25,8 +23,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
sensors = hass.data[DATA_DECONZ].sensors
entities = []
for key in sorted(sensors.keys(), key=int):
sensor = sensors[key]
for sensor in sensors.values():
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True)
@@ -39,8 +36,7 @@ class DeconzBinarySensor(BinarySensorDevice):
"""Set up sensor and add update callback to get data from websocket."""
self._sensor = sensor
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
@@ -96,9 +92,9 @@ class DeconzBinarySensor(BinarySensorDevice):
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE
attr = {
ATTR_BATTERY_LEVEL: self._sensor.battery,
}
if self._sensor.type in PRESENCE:
attr = {}
if self._sensor.battery:
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
if self._sensor.type in PRESENCE and self._sensor.dark:
attr['dark'] = self._sensor.dark
return attr

View File

@@ -12,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.egardia import (
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['egardia']
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
'Door Contact': 'opening',
'IR': 'motion'}

View File

@@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {'openClosedSensor': 'opening',
'motionSensor': 'motion',
'doorSensor': 'door',
'leakSensor': 'moisture'}
'wetLeakSensor': 'moisture'}
@asyncio.coroutine
@@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
address = discovery_info['address']
device = plm.devices[address]
state_key = discovery_info['state_key']
name = device.states[state_key].name
if name != 'dryLeakSensor':
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
device.address.hex, device.states[state_key].name)
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
device.address.hex, device.states[state_key].name)
new_entity = InsteonPLMBinarySensor(device, state_key)
new_entity = InsteonPLMBinarySensor(device, state_key)
async_add_devices([new_entity])
async_add_devices([new_entity])
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
@@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
@property
def is_on(self):
"""Return the boolean response if the node is on."""
sensorstate = self._insteon_device_state.value
return bool(sensorstate)
return bool(self._insteon_device_state.value)

View File

@@ -1,97 +0,0 @@
"""
Support for Mercedes cars with Mercedes ME.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mercedesme/
"""
import logging
import datetime
from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.mercedesme import (
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
DEPENDENCIES = ['mercedesme']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the sensor platform."""
data = hass.data[DATA_MME].data
if not data.cars:
_LOGGER.error("No cars found. Check component log.")
return
devices = []
for car in data.cars:
for key, value in sorted(BINARY_SENSORS.items()):
if car['availabilities'].get(key, 'INVALID') == 'VALID':
devices.append(MercedesMEBinarySensor(
data, key, value[0], car["vin"], None))
else:
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
add_devices(devices, True)
class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
"""Representation of a Sensor."""
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._internal_name == "windowsClosed":
return {
"window_front_left": self._car["windowStatusFrontLeft"],
"window_front_right": self._car["windowStatusFrontRight"],
"window_rear_left": self._car["windowStatusRearLeft"],
"window_rear_right": self._car["windowStatusRearRight"],
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"]
}
elif self._internal_name == "tireWarningLight":
return {
"front_right_tire_pressure_kpa":
self._car["frontRightTirePressureKpa"],
"front_left_tire_pressure_kpa":
self._car["frontLeftTirePressureKpa"],
"rear_right_tire_pressure_kpa":
self._car["rearRightTirePressureKpa"],
"rear_left_tire_pressure_kpa":
self._car["rearLeftTirePressureKpa"],
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]
).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"],
}
return {
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"]
}
def update(self):
"""Fetch new state data for the sensor."""
self._car = next(
car for car in self._data.cars if car["vin"] == self._vin)
if self._internal_name == "windowsClosed":
self._state = bool(self._car[self._internal_name] == "CLOSED")
elif self._internal_name == "tireWarningLight":
self._state = bool(self._car[self._internal_name] != "INACTIVE")
else:
self._state = self._car[self._internal_name] is True
_LOGGER.debug("Updated %s Value: %s IsOn: %s",
self._internal_name, self._state, self.is_on)

View File

@@ -9,12 +9,24 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASSES, DOMAIN, BinarySensorDevice)
from homeassistant.const import STATE_ON
SENSORS = {
'S_DOOR': 'door',
'S_MOTION': 'motion',
'S_SMOKE': 'smoke',
'S_SPRINKLER': 'safety',
'S_WATER_LEAK': 'safety',
'S_SOUND': 'sound',
'S_VIBRATION': 'vibration',
'S_MOISTURE': 'moisture',
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors platform for binary sensors."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for binary sensors."""
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
add_devices=add_devices)
async_add_devices=async_add_devices)
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
@@ -29,18 +41,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
pres = self.gateway.const.Presentation
class_map = {
pres.S_DOOR: 'opening',
pres.S_MOTION: 'motion',
pres.S_SMOKE: 'smoke',
}
if float(self.gateway.protocol_version) >= 1.5:
class_map.update({
pres.S_SPRINKLER: 'sprinkler',
pres.S_WATER_LEAK: 'leak',
pres.S_SOUND: 'sound',
pres.S_VIBRATION: 'vibration',
pres.S_MOISTURE: 'moisture',
})
if class_map.get(self.child_type) in DEVICE_CLASSES:
return class_map.get(self.child_type)
device_class = SENSORS.get(pres(self.child_type).name)
if device_class in DEVICE_CLASSES:
return device_class
return None

View File

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

View File

@@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.3']
REQUIREMENTS = ['holidays==0.9.4']
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
@@ -30,8 +30,8 @@ ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US',
'Wales']
'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
'UnitedStates', 'US', 'Wales']
CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
@@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
})
@@ -74,14 +74,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if province:
# 'state' and 'prov' are not interchangeable, so need to make
# sure we use the right one
if (hasattr(obj_holidays, "PROVINCES") and
if (hasattr(obj_holidays, 'PROVINCES') and
province in obj_holidays.PROVINCES):
obj_holidays = getattr(holidays, country)(prov=province,
years=year)
elif (hasattr(obj_holidays, "STATES") and
obj_holidays = getattr(holidays, country)(
prov=province, years=year)
elif (hasattr(obj_holidays, 'STATES') and
province in obj_holidays.STATES):
obj_holidays = getattr(holidays, country)(state=province,
years=year)
obj_holidays = getattr(holidays, country)(
state=province, years=year)
else:
_LOGGER.error("There is no province/state %s in country %s",
province, country)

View File

@@ -4,30 +4,29 @@ Reads vehicle status from BMW connected drive portal.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/bmw_connected_drive/
"""
import logging
import datetime
import logging
import voluptuous as vol
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD
)
REQUIREMENTS = ['bimmer_connected==0.4.1']
REQUIREMENTS = ['bimmer_connected==0.5.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
CONF_VALUES = 'values'
CONF_COUNTRY = 'country'
CONF_REGION = 'region'
ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_COUNTRY): cv.string,
vol.Required(CONF_REGION): vol.Any('north_america', 'china',
'rest_of_world'),
})
CONFIG_SCHEMA = vol.Schema({
@@ -37,7 +36,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
BMW_COMPONENTS = ['device_tracker', 'sensor']
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes
@@ -47,9 +46,9 @@ def setup(hass, config):
for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
country = account_config[CONF_COUNTRY]
region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name)
bimmer = BMWConnectedDriveAccount(username, password, country, name)
bimmer = BMWConnectedDriveAccount(username, password, region, name)
accounts.append(bimmer)
# update every UPDATE_INTERVAL minutes, starting now
@@ -75,12 +74,15 @@ def setup(hass, config):
class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""
def __init__(self, username: str, password: str, country: str,
def __init__(self, username: str, password: str, region_str: str,
name: str) -> None:
"""Constructor."""
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
self.account = ConnectedDriveAccount(username, password, country)
region = get_region_from_name(region_str)
self.account = ConnectedDriveAccount(username, password, region)
self.name = name
self._update_listeners = []

View File

@@ -194,7 +194,9 @@ class WebDavCalendarData(object):
@staticmethod
def is_over(vevent):
"""Return if the event is over."""
return dt.now() > WebDavCalendarData.get_end_date(vevent)
return dt.now() >= WebDavCalendarData.to_datetime(
WebDavCalendarData.get_end_date(vevent)
)
@staticmethod
def get_hass_date(obj):
@@ -230,4 +232,4 @@ class WebDavCalendarData(object):
else:
enddate = obj.dtstart.value + timedelta(days=1)
return WebDavCalendarData.to_datetime(enddate)
return enddate

View File

@@ -62,7 +62,14 @@ class GoogleCalendarData(object):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
service = self.calendar_service.get()
from httplib2 import ServerNotFoundError
try:
service = self.calendar_service.get()
except ServerNotFoundError:
_LOGGER.warning("Unable to connect to Google, using cached data")
return False
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id

View File

@@ -496,6 +496,10 @@ class TodoistProjectData(object):
# We had no valid tasks
return True
# Make sure the task collection is reset to prevent an
# infinite collection repeating the same tasks
self.all_project_tasks.clear()
# Organize the best tasks (so users can see all the tasks
# they have, organized)
while project_tasks:

View File

@@ -21,7 +21,7 @@ from homeassistant.components.camera import (
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.util.async_ import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)

View File

@@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic'
DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt']
@@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT Camera."""
topic = config[CONF_TOPIC]
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices([MqttCamera(config[CONF_NAME], topic)])
async_add_devices([MqttCamera(
config.get(CONF_NAME),
config.get(CONF_TOPIC)
)])
class MqttCamera(Camera):

View File

@@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
"""
import asyncio
import logging
import os
import voluptuous as vol
@@ -103,92 +104,128 @@ class ONVIFHassCamera(Camera):
def __init__(self, hass, config):
"""Initialize a ONVIF camera."""
from onvif import ONVIFCamera, exceptions
super().__init__()
import onvif
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._host = config.get(CONF_HOST)
self._port = config.get(CONF_PORT)
self._name = config.get(CONF_NAME)
self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
self._profile_index = config.get(CONF_PROFILE)
self._input = None
camera = None
self._media_service = \
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
self._host, self._port),
self._username, self._password,
'{}/wsdl/media.wsdl'.format(os.path.dirname(
onvif.__file__)))
self._ptz_service = \
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
self._host, self._port),
self._username, self._password,
'{}/wsdl/ptz.wsdl'.format(os.path.dirname(
onvif.__file__)))
def obtain_input_uri(self):
"""Set the input uri for the camera."""
from onvif import exceptions
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
self._host, self._port)
try:
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
config.get(CONF_HOST), config.get(CONF_PORT))
camera = ONVIFCamera(
config.get(CONF_HOST), config.get(CONF_PORT),
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
)
media_service = camera.create_media_service()
self._profiles = media_service.GetProfiles()
self._profile_index = config.get(CONF_PROFILE)
if self._profile_index >= len(self._profiles):
profiles = self._media_service.GetProfiles()
if self._profile_index >= len(profiles):
_LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d."
" Using the last profile.",
self._name, self._profile_index)
self._profile_index = -1
req = media_service.create_type('GetStreamUri')
req = self._media_service.create_type('GetStreamUri')
# pylint: disable=protected-access
req.ProfileToken = self._profiles[self._profile_index]._token
self._input = media_service.GetStreamUri(req).Uri.replace(
'rtsp://', 'rtsp://{}:{}@'.format(
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD)), 1)
req.ProfileToken = profiles[self._profile_index]._token
uri_no_auth = self._media_service.GetStreamUri(req).Uri
uri_for_log = uri_no_auth.replace(
'rtsp://', 'rtsp://<user>:<password>@', 1)
self._input = uri_no_auth.replace(
'rtsp://', 'rtsp://{}:{}@'.format(self._username,
self._password), 1)
_LOGGER.debug(
"ONVIF Camera Using the following URL for %s: %s",
self._name, self._input)
except Exception as err:
_LOGGER.error("Unable to communicate with ONVIF Camera: %s", err)
raise
try:
self._ptz = camera.create_ptz_service()
self._name, uri_for_log)
# we won't need the media service anymore
self._media_service = None
except exceptions.ONVIFError as err:
self._ptz = None
_LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err)
_LOGGER.debug("Couldn't setup camera '%s'. Error: %s",
self._name, err)
return
def perform_ptz(self, pan, tilt, zoom):
"""Perform a PTZ action on the camera."""
if self._ptz:
from onvif import exceptions
if self._ptz_service:
pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
req = {"Velocity": {
"PanTilt": {"_x": pan_val, "_y": tilt_val},
"Zoom": {"_x": zoom_val}}}
self._ptz.ContinuousMove(req)
try:
self._ptz_service.ContinuousMove(req)
except exceptions.ONVIFError as err:
if "Bad Request" in err.reason:
self._ptz_service = None
_LOGGER.debug("Camera '%s' doesn't support PTZ.",
self._name)
else:
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Callback when entity is added to hass."""
if ONVIF_DATA not in self.hass.data:
self.hass.data[ONVIF_DATA] = {}
self.hass.data[ONVIF_DATA][ENTITIES] = []
self.hass.data[ONVIF_DATA][ENTITIES].append(self)
@asyncio.coroutine
def async_camera_image(self):
async def async_camera_image(self):
"""Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG
if not self._input:
await self.hass.async_add_job(self.obtain_input_uri)
if not self._input:
return None
ffmpeg = ImageFrame(
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
image = yield from asyncio.shield(ffmpeg.get_image(
image = await asyncio.shield(ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
return image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
if not self._input:
await self.hass.async_add_job(self.obtain_input_uri)
if not self._input:
return None
stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary,
loop=self.hass.loop)
yield from stream.open_camera(
await stream.open_camera(
self._input, extra_cmd=self._ffmpeg_arguments)
yield from async_aiohttp_proxy_stream(
await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close()
await stream.close()
@property
def name(self):

View File

@@ -11,7 +11,7 @@ import async_timeout
import voluptuous as vol
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.util.async_ import run_coroutine_threadsafe
from homeassistant.helpers import config_validation as cv
import homeassistant.util.dt as dt_util
@@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices,
async_add_devices([ProxyCamera(hass, config)])
async def _read_frame(req):
"""Read a single frame from an MJPEG stream."""
# based on https://gist.github.com/russss/1143799
import cgi
# Read in HTTP headers:
stream = req.content
# multipart/x-mixed-replace; boundary=--frameboundary
_mimetype, options = cgi.parse_header(req.headers['content-type'])
boundary = options.get('boundary').encode('utf-8')
if not boundary:
_LOGGER.error("Malformed MJPEG missing boundary")
raise Exception("Can't find content-type")
line = await stream.readline()
# Seek ahead to the first chunk
while line.strip() != boundary:
line = await stream.readline()
# Read in chunk headers
while line.strip() != b'':
parts = line.split(b':')
if len(parts) > 1 and parts[0].lower() == b'content-length':
# Grab chunk length
length = int(parts[1].strip())
line = await stream.readline()
image = await stream.read(length)
return image
def _resize_image(image, opts):
"""Resize image."""
from PIL import Image
@@ -227,9 +199,9 @@ class ProxyCamera(Camera):
'boundary=--frameboundary')
await response.prepare(request)
def write(img_bytes):
async def write(img_bytes):
"""Write image to stream."""
response.write(bytes(
await response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
@@ -240,13 +212,23 @@ class ProxyCamera(Camera):
req = await stream_coro
try:
# This would be nicer as an async generator
# But that would only be supported for python >=3.6
data = b''
stream = req.content
while True:
image = await _read_frame(req)
if not image:
chunk = await stream.read(102400)
if not chunk:
break
image = await self.hass.async_add_job(
_resize_image, image, self._stream_opts)
write(image)
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
image = data[jpg_start:jpg_end + 2]
image = await self.hass.async_add_job(
_resize_image, image, self._stream_opts)
await write(image)
data = data[jpg_end + 2:]
except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.")
req.close()

View File

@@ -4,7 +4,6 @@ Support for Xeoma Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.xeoma/
"""
import asyncio
import logging
import voluptuous as vol
@@ -14,7 +13,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['pyxeoma==1.3']
REQUIREMENTS = ['pyxeoma==1.4.0']
_LOGGER = logging.getLogger(__name__)
@@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
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):
"""Discover and setup Xeoma Cameras."""
from pyxeoma.xeoma import Xeoma, XeomaError
@@ -53,8 +52,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
xeoma = Xeoma(host, login, password)
try:
yield from xeoma.async_test_connection()
discovered_image_names = yield from xeoma.async_get_image_names()
await xeoma.async_test_connection()
discovered_image_names = await xeoma.async_get_image_names()
discovered_cameras = [
{
CONF_IMAGE_NAME: image_name,
@@ -103,12 +102,11 @@ class XeomaCamera(Camera):
self._password = password
self._last_image = None
@asyncio.coroutine
def async_camera_image(self):
async def async_camera_image(self):
"""Return a still image response from the camera."""
from pyxeoma.xeoma import XeomaError
try:
image = yield from self._xeoma.async_get_camera_image(
image = await self._xeoma.async_get_camera_image(
self._image, self._username, self._password)
self._last_image = image
except XeomaError as err:

View File

@@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
REQUIREMENTS = ['py-canary==0.4.1']
REQUIREMENTS = ['py-canary==0.5.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -14,10 +14,10 @@ from homeassistant.components.climate import (
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_LOW)
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE,
SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
_CONFIGURING = {}
@@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW)
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -122,6 +122,7 @@ class Thermostat(ClimateDevice):
self._climate_list = self.climate_list
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
'heat', 'off']
self._fan_list = ['auto', 'on']
self.update_without_throttle = False
def update(self):
@@ -180,24 +181,29 @@ class Thermostat(ClimateDevice):
return self.thermostat['runtime']['desiredCool'] / 10.0
return None
@property
def desired_fan_mode(self):
"""Return the desired fan mode of operation."""
return self.thermostat['runtime']['desiredFanMode']
@property
def fan(self):
"""Return the current fan state."""
"""Return the current fan status."""
if 'fan' in self.thermostat['equipmentStatus']:
return STATE_ON
return STATE_OFF
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self.thermostat['runtime']['desiredFanMode']
@property
def current_hold_mode(self):
"""Return current hold mode."""
mode = self._current_hold_mode
return None if mode == AWAY_MODE else mode
@property
def fan_list(self):
"""Return the available fan modes."""
return self._fan_list
@property
def _current_hold_mode(self):
events = self.thermostat['events']
@@ -206,7 +212,7 @@ class Thermostat(ClimateDevice):
if event['type'] == 'hold':
if event['holdClimateRef'] == 'away':
if int(event['endDate'][0:4]) - \
int(event['startDate'][0:4]) <= 1:
int(event['startDate'][0:4]) <= 1:
# A temporary hold from away climate is a hold
return 'away'
# A permanent hold from away climate
@@ -228,7 +234,7 @@ class Thermostat(ClimateDevice):
def current_operation(self):
"""Return current operation."""
if self.operation_mode == 'auxHeatOnly' or \
self.operation_mode == 'heatPump':
self.operation_mode == 'heatPump':
return STATE_HEAT
return self.operation_mode
@@ -271,10 +277,11 @@ class Thermostat(ClimateDevice):
operation = STATE_HEAT
else:
operation = status
return {
"actual_humidity": self.thermostat['runtime']['actualHumidity'],
"fan": self.fan,
"mode": self.mode,
"climate_mode": self.mode,
"operation": operation,
"climate_list": self.climate_list,
"fan_min_on_time": self.fan_min_on_time
@@ -342,25 +349,46 @@ class Thermostat(ClimateDevice):
cool_temp_setpoint, heat_temp_setpoint,
self.hold_preference())
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
"cool=%s, is=%s", heat_temp, isinstance(
heat_temp, (int, float)), cool_temp,
"cool=%s, is=%s", heat_temp,
isinstance(heat_temp, (int, float)), cool_temp,
isinstance(cool_temp, (int, float)))
self.update_without_throttle = True
def set_fan_mode(self, fan_mode):
"""Set the fan mode. Valid values are "on" or "auto"."""
if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO):
error = "Invalid fan_mode value: Valid values are 'on' or 'auto'"
_LOGGER.error(error)
return
cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0
heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0
self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode,
cool_temp, heat_temp,
self.hold_preference())
_LOGGER.info("Setting fan mode to: %s", fan_mode)
def set_temp_hold(self, temp):
"""Set temperature hold in modes other than auto."""
# Set arbitrary range when not in auto mode
if self.current_operation == STATE_HEAT:
"""Set temperature hold in modes other than auto.
Ecobee API: It is good practice to set the heat and cool hold
temperatures to be the same, if the thermostat is in either heat, cool,
auxHeatOnly, or off mode. If the thermostat is in auto mode, an
additional rule is required. The cool hold temperature must be greater
than the heat hold temperature by at least the amount in the
heatCoolMinDelta property.
https://www.ecobee.com/home/developer/api/examples/ex5.shtml
"""
if self.current_operation == STATE_HEAT or self.current_operation == \
STATE_COOL:
heat_temp = temp
cool_temp = temp + 20
elif self.current_operation == STATE_COOL:
heat_temp = temp - 20
cool_temp = temp
else:
# In auto mode set temperature between
heat_temp = temp - 10
cool_temp = temp + 10
delta = self.thermostat['settings']['heatCoolMinDelta'] / 10
heat_temp = temp - delta
cool_temp = temp + delta
self.set_auto_temp_hold(heat_temp, cool_temp)
def set_temperature(self, **kwargs):
@@ -369,8 +397,8 @@ class Thermostat(ClimateDevice):
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
temp = kwargs.get(ATTR_TEMPERATURE)
if self.current_operation == STATE_AUTO and (low_temp is not None or
high_temp is not None):
if self.current_operation == STATE_AUTO and \
(low_temp is not None or high_temp is not None):
self.set_auto_temp_hold(low_temp, high_temp)
elif temp is not None:
self.set_temp_hold(temp)

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-eq3bt==0.1.9']
REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41']
_LOGGER = logging.getLogger(__name__)

View File

@@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors climate."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors climate."""
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices)
hass, DOMAIN, discovery_info, MySensorsHVAC,
async_add_devices=async_add_devices)
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
@@ -163,8 +165,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
self._values[self.value_type] = operation_mode
self.schedule_update_ha_state()
def update(self):
async def async_update(self):
"""Update the controller with the latest value from a sensor."""
super().update()
await super().async_update()
self._values[self.value_type] = DICT_MYS_TO_HA[
self._values[self.value_type]]

View File

@@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice):
try:
self.device.target = temp
except nest.nest.APIError:
_LOGGER.error("An error occured while setting the temperature")
_LOGGER.error("An error occurred while setting the temperature")
def set_operation_mode(self, operation_mode):
"""Set operation mode."""

View File

@@ -248,6 +248,11 @@ class SensiboClimate(ClimateDevice):
return self._temperatures_list[-1] \
if self._temperatures_list else super().max_temp
@property
def unique_id(self):
"""Return unique ID based on Sensibo ID."""
return self._id
@asyncio.coroutine
def async_set_temperature(self, **kwargs):
"""Set new target temperature."""

View File

@@ -37,6 +37,7 @@ CONF_FILTER = 'filter'
CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
@@ -75,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}),
@@ -110,7 +112,7 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None,
relayer=None):
relayer=None, google_actions_sync_url=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
@@ -128,6 +130,7 @@ class Cloud:
self.user_pool_id = user_pool_id
self.region = region
self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url
else:
info = SERVERS[mode]
@@ -136,6 +139,7 @@ class Cloud:
self.user_pool_id = info['user_pool_id']
self.region = info['region']
self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url']
@property
def is_logged_in(self):

View File

@@ -8,7 +8,9 @@ SERVERS = {
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
'user_pool_id': 'us-east-1_87ll5WOP8',
'region': 'us-east-1',
'relayer': 'wss://cloud.hass.io:8000/websocket'
'relayer': 'wss://cloud.hass.io:8000/websocket',
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
'amazonaws.com/prod/smart_home_sync'),
}
}

View File

@@ -16,9 +16,9 @@ from .const import DOMAIN, REQUEST_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup(hass):
async def async_setup(hass):
"""Initialize the HTTP API."""
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
hass.http.register_view(CloudAccountView)
@@ -38,12 +38,11 @@ _CLOUD_ERRORS = {
def _handle_cloud_errors(handler):
"""Handle auth errors."""
@asyncio.coroutine
@wraps(handler)
def error_handler(view, request, *args, **kwargs):
async def error_handler(view, request, *args, **kwargs):
"""Handle exceptions that raise from the wrapped request handler."""
try:
result = yield from handler(view, request, *args, **kwargs)
result = await handler(view, request, *args, **kwargs)
return result
except (auth_api.CloudError, asyncio.TimeoutError) as err:
@@ -57,6 +56,31 @@ def _handle_cloud_errors(handler):
return error_handler
class GoogleActionsSyncView(HomeAssistantView):
"""Trigger a Google Actions Smart Home Sync."""
url = '/api/cloud/google_actions/sync'
name = 'api:cloud:google_actions/sync'
@_handle_cloud_errors
async def post(self, request):
"""Trigger a Google Actions sync."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
websession = hass.helpers.aiohttp_client.async_get_clientsession()
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(auth_api.check_token, cloud)
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
req = await websession.post(
cloud.google_actions_sync_url, headers={
'authorization': cloud.id_token
})
return self.json({}, status_code=req.status)
class CloudLoginView(HomeAssistantView):
"""Login to Home Assistant cloud."""
@@ -68,19 +92,18 @@ class CloudLoginView(HomeAssistantView):
vol.Required('email'): str,
vol.Required('password'): str,
}))
@asyncio.coroutine
def post(self, request, data):
async def post(self, request, data):
"""Handle login request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
data['password'])
await hass.async_add_job(auth_api.login, cloud, data['email'],
data['password'])
hass.async_add_job(cloud.iot.connect)
# Allow cloud to start connecting.
yield from asyncio.sleep(0, loop=hass.loop)
await asyncio.sleep(0, loop=hass.loop)
return self.json(_account_data(cloud))
@@ -91,14 +114,13 @@ class CloudLogoutView(HomeAssistantView):
name = 'api:cloud:logout'
@_handle_cloud_errors
@asyncio.coroutine
def post(self, request):
async def post(self, request):
"""Handle logout request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from cloud.logout()
await cloud.logout()
return self.json_message('ok')
@@ -109,8 +131,7 @@ class CloudAccountView(HomeAssistantView):
url = '/api/cloud/account'
name = 'api:cloud:account'
@asyncio.coroutine
def get(self, request):
async def get(self, request):
"""Get account info."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
@@ -132,14 +153,13 @@ class CloudRegisterView(HomeAssistantView):
vol.Required('email'): str,
vol.Required('password'): vol.All(str, vol.Length(min=6)),
}))
@asyncio.coroutine
def post(self, request, data):
async def post(self, request, data):
"""Handle registration request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
await hass.async_add_job(
auth_api.register, cloud, data['email'], data['password'])
return self.json_message('ok')
@@ -155,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView):
@RequestDataValidator(vol.Schema({
vol.Required('email'): str,
}))
@asyncio.coroutine
def post(self, request, data):
async def post(self, request, data):
"""Handle resending confirm email code request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
await hass.async_add_job(
auth_api.resend_email_confirm, cloud, data['email'])
return self.json_message('ok')
@@ -178,14 +197,13 @@ class CloudForgotPasswordView(HomeAssistantView):
@RequestDataValidator(vol.Schema({
vol.Required('email'): str,
}))
@asyncio.coroutine
def post(self, request, data):
async def post(self, request, data):
"""Handle forgot password request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
await hass.async_add_job(
auth_api.forgot_password, cloud, data['email'])
return self.json_message('ok')

View File

@@ -14,24 +14,16 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
'entity_registry')
'entity_registry', 'config_entries')
ON_DEMAND = ('zwave',)
FEATURE_FLAGS = ('config_entries',)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the config component."""
global SECTIONS
yield from hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'mdi:settings')
# Temporary way of allowing people to opt-in for unreleased config sections
for key, value in config.get(DOMAIN, {}).items():
if key in FEATURE_FLAGS and value:
SECTIONS += (key,)
@asyncio.coroutine
def setup_panel(panel_name):
"""Set up a panel."""

View File

@@ -1,102 +0,0 @@
"""Example component to show how config entries work."""
import asyncio
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.util import slugify
DOMAIN = 'config_entry_example'
@asyncio.coroutine
def async_setup(hass, config):
"""Setup for our example component."""
return True
@asyncio.coroutine
def async_setup_entry(hass, entry):
"""Initialize an entry."""
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
hass.states.async_set(entity_id, 'loaded', {
ATTR_FRIENDLY_NAME: entry.data['name']
})
# Indicate setup was successful.
return True
@asyncio.coroutine
def async_unload_entry(hass, entry):
"""Unload an entry."""
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
hass.states.async_remove(entity_id)
# Indicate unload was successful.
return True
@config_entries.HANDLERS.register(DOMAIN)
class ExampleConfigFlow(config_entries.ConfigFlowHandler):
"""Handle an example configuration flow."""
VERSION = 1
def __init__(self):
"""Initialize a Hue config handler."""
self.object_id = None
@asyncio.coroutine
def async_step_init(self, user_input=None):
"""Start config flow."""
errors = None
if user_input is not None:
object_id = user_input['object_id']
if object_id != '' and object_id == slugify(object_id):
self.object_id = user_input['object_id']
return (yield from self.async_step_name())
errors = {
'object_id': 'Invalid object id.'
}
return self.async_show_form(
title='Pick object id',
step_id='init',
description="Please enter an object_id for the test entity.",
data_schema=vol.Schema({
'object_id': str
}),
errors=errors
)
@asyncio.coroutine
def async_step_name(self, user_input=None):
"""Ask user to enter the name."""
errors = None
if user_input is not None:
name = user_input['name']
if name != '':
return self.async_create_entry(
title=name,
data={
'name': name,
'object_id': self.object_id,
}
)
return self.async_show_form(
title='Name of the entity',
step_id='name',
description="Please enter a name for the test entity.",
data_schema=vol.Schema({
'name': str
}),
errors=errors
)

View File

@@ -15,7 +15,7 @@ from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
ATTR_ENTITY_PICTURE
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.util.async import run_callback_threadsafe
from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
_KEY_INSTANCE = 'configurator'

View File

@@ -13,10 +13,14 @@ from homeassistant import core
from homeassistant.components import http
from homeassistant.components.http.data_validator import (
RequestDataValidator)
from homeassistant.components.cover import (INTENT_OPEN_COVER,
INTENT_CLOSE_COVER)
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import intent
from homeassistant.loader import bind_hass
from homeassistant.setup import (ATTR_COMPONENT)
_LOGGER = logging.getLogger(__name__)
@@ -28,6 +32,13 @@ DOMAIN = 'conversation'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REGEX_TYPE = type(re.compile(''))
UTTERANCES = {
'cover': {
INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'],
INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]']
}
}
SERVICE_PROCESS = 'process'
SERVICE_PROCESS_SCHEMA = vol.Schema({
@@ -112,6 +123,25 @@ async def async_setup(hass, config):
'[the] [a] [an] {name}[s] toggle',
])
@callback
def register_utterances(component):
"""Register utterances for a component."""
if component not in UTTERANCES:
return
for intent_type, sentences in UTTERANCES[component].items():
async_register(hass, intent_type, sentences)
@callback
def component_loaded(event):
"""Handle a new component loaded."""
register_utterances(event.data[ATTR_COMPONENT])
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
# Check already loaded components.
for component in hass.config.components:
register_utterances(component)
return True

View File

@@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.components import group
from homeassistant.helpers import intent
from homeassistant.const import (
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
@@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
ATTR_POSITION = 'position'
ATTR_TILT_POSITION = 'tilt_position'
INTENT_OPEN_COVER = 'HassOpenCover'
INTENT_CLOSE_COVER = 'HassCloseCover'
COVER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@@ -181,6 +185,12 @@ async def async_setup(hass, config):
hass.services.async_register(
DOMAIN, service_name, async_handle_cover_service,
schema=schema)
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER,
"Opened {}"))
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER,
"Closed {}"))
return True

View File

@@ -0,0 +1,120 @@
"""
Support for Gogogate2 Garage Doors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.gogogate2/
"""
import logging
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_IP_ADDRESS, CONF_NAME)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pygogogate2==0.0.3']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'gogogate2'
NOTIFICATION_ID = 'gogogate2_notification'
NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
COVER_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Gogogate2 component."""
from pygogogate2 import Gogogate2API as pygogogate2
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
ip_address = config.get(CONF_IP_ADDRESS)
name = config.get(CONF_NAME)
mygogogate2 = pygogogate2(username, password, ip_address)
try:
devices = mygogogate2.get_devices()
if devices is False:
raise ValueError(
"Username or Password is incorrect or no devices found")
add_devices(MyGogogate2Device(
mygogogate2, door, name) for door in devices)
return
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return
class MyGogogate2Device(CoverDevice):
"""Representation of a Gogogate2 cover."""
def __init__(self, mygogogate2, device, name):
"""Initialize with API object, device id."""
self.mygogogate2 = mygogogate2
self.device_id = device['door']
self._name = name or device['name']
self._status = device['status']
self.available = None
@property
def name(self):
"""Return the name of the garage door if any."""
return self._name if self._name else DEFAULT_NAME
@property
def is_closed(self):
"""Return true if cover is closed, else False."""
return self._status == STATE_CLOSED
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'garage'
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE
@property
def available(self):
"""Could the device be accessed during the last update call."""
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
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
self._status = STATE_UNKNOWN
self.available = False

View File

@@ -0,0 +1,271 @@
"""
This platform allows several cover to be grouped into one cover.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.group/
"""
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.cover import (
DOMAIN, PLATFORM_SCHEMA, CoverDevice, ATTR_POSITION,
ATTR_CURRENT_POSITION, ATTR_TILT_POSITION, ATTR_CURRENT_TILT_POSITION,
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION,
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT,
SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION,
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION)
from homeassistant.const import (
ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
CONF_ENTITIES, CONF_NAME, STATE_CLOSED)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__)
KEY_OPEN_CLOSE = 'open_close'
KEY_STOP = 'stop'
KEY_POSITION = 'position'
DEFAULT_NAME = 'Cover Group'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN),
})
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Group Cover platform."""
async_add_devices(
[CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])])
class CoverGroup(CoverDevice):
"""Representation of a CoverGroup."""
def __init__(self, name, entities):
"""Initialize a CoverGroup entity."""
self._name = name
self._is_closed = False
self._cover_position = 100
self._tilt_position = None
self._supported_features = 0
self._assumed_state = True
self._entities = entities
self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(),
KEY_POSITION: set()}
self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(),
KEY_POSITION: set()}
@callback
def update_supported_features(self, entity_id, old_state, new_state,
update_state=True):
"""Update dictionaries with supported features."""
if not new_state:
for values in self._covers.values():
values.discard(entity_id)
for values in self._tilts.values():
values.discard(entity_id)
if update_state:
self.async_schedule_update_ha_state(True)
return
features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & (SUPPORT_OPEN | SUPPORT_CLOSE):
self._covers[KEY_OPEN_CLOSE].add(entity_id)
else:
self._covers[KEY_OPEN_CLOSE].discard(entity_id)
if features & (SUPPORT_STOP):
self._covers[KEY_STOP].add(entity_id)
else:
self._covers[KEY_STOP].discard(entity_id)
if features & (SUPPORT_SET_POSITION):
self._covers[KEY_POSITION].add(entity_id)
else:
self._covers[KEY_POSITION].discard(entity_id)
if features & (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT):
self._tilts[KEY_OPEN_CLOSE].add(entity_id)
else:
self._tilts[KEY_OPEN_CLOSE].discard(entity_id)
if features & (SUPPORT_STOP_TILT):
self._tilts[KEY_STOP].add(entity_id)
else:
self._tilts[KEY_STOP].discard(entity_id)
if features & (SUPPORT_SET_TILT_POSITION):
self._tilts[KEY_POSITION].add(entity_id)
else:
self._tilts[KEY_POSITION].discard(entity_id)
if update_state:
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register listeners."""
for entity_id in self._entities:
new_state = self.hass.states.get(entity_id)
self.update_supported_features(entity_id, None, new_state,
update_state=False)
async_track_state_change(self.hass, self._entities,
self.update_supported_features)
await self.async_update()
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def assumed_state(self):
"""Enable buttons even if at end position."""
return self._assumed_state
@property
def should_poll(self):
"""Disable polling for cover group."""
return False
@property
def supported_features(self):
"""Flag supported features for the cover."""
return self._supported_features
@property
def is_closed(self):
"""Return if all covers in group are closed."""
return self._is_closed
@property
def current_cover_position(self):
"""Return current position for all covers."""
return self._cover_position
@property
def current_cover_tilt_position(self):
"""Return current tilt position for all covers."""
return self._tilt_position
async def async_open_cover(self, **kwargs):
"""Move the covers up."""
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_OPEN_COVER, data, blocking=True)
async def async_close_cover(self, **kwargs):
"""Move the covers down."""
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True)
async def async_stop_cover(self, **kwargs):
"""Fire the stop action."""
data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]}
await self.hass.services.async_call(
DOMAIN, SERVICE_STOP_COVER, data, blocking=True)
async def async_set_cover_position(self, **kwargs):
"""Set covers position."""
data = {ATTR_ENTITY_ID: self._covers[KEY_POSITION],
ATTR_POSITION: kwargs[ATTR_POSITION]}
await self.hass.services.async_call(
DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True)
async def async_open_cover_tilt(self, **kwargs):
"""Tilt covers open."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True)
async def async_close_cover_tilt(self, **kwargs):
"""Tilt covers closed."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True)
async def async_stop_cover_tilt(self, **kwargs):
"""Stop cover tilt."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]}
await self.hass.services.async_call(
DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True)
async def async_set_cover_tilt_position(self, **kwargs):
"""Set tilt position."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_POSITION],
ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION]}
await self.hass.services.async_call(
DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True)
async def async_update(self):
"""Update state and attributes."""
self._assumed_state = False
self._is_closed = True
for entity_id in self._entities:
state = self.hass.states.get(entity_id)
if not state:
continue
if state.state != STATE_CLOSED:
self._is_closed = False
break
self._cover_position = None
if self._covers[KEY_POSITION]:
position = -1
self._cover_position = 0 if self.is_closed else 100
for entity_id in self._covers[KEY_POSITION]:
state = self.hass.states.get(entity_id)
pos = state.attributes.get(ATTR_CURRENT_POSITION)
if position == -1:
position = pos
elif position != pos:
self._assumed_state = True
break
else:
if position != -1:
self._cover_position = position
self._tilt_position = None
if self._tilts[KEY_POSITION]:
position = -1
self._tilt_position = 100
for entity_id in self._tilts[KEY_POSITION]:
state = self.hass.states.get(entity_id)
pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION)
if position == -1:
position = pos
elif position != pos:
self._assumed_state = True
break
else:
if position != -1:
self._tilt_position = position
supported_features = 0
supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE \
if self._covers[KEY_OPEN_CLOSE] else 0
supported_features |= SUPPORT_STOP \
if self._covers[KEY_STOP] else 0
supported_features |= SUPPORT_SET_POSITION \
if self._covers[KEY_POSITION] else 0
supported_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT \
if self._tilts[KEY_OPEN_CLOSE] else 0
supported_features |= SUPPORT_STOP_TILT \
if self._tilts[KEY_STOP] else 0
supported_features |= SUPPORT_SET_TILT_POSITION \
if self._tilts[KEY_POSITION] else 0
self._supported_features = supported_features
if not self._assumed_state:
for entity_id in self._entities:
state = self.hass.states.get(entity_id)
if state and state.attributes.get(ATTR_ASSUMED_STATE):
self._assumed_state = True
break

View File

@@ -9,10 +9,12 @@ from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice
from homeassistant.const import STATE_OFF, STATE_ON
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors platform for covers."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for covers."""
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices)
hass, DOMAIN, discovery_info, MySensorsCover,
async_add_devices=async_add_devices)
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):

View File

@@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice):
self._invert_relay = invert_relay
rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
@property
def name(self):
@@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice):
def _trigger(self):
"""Trigger the cover."""
rpi_gpio.write_output(self._relay_pin, self._invert_relay)
rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0)
sleep(self._relay_time)
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
def close_cover(self, **kwargs):
"""Close the cover."""

View File

@@ -234,7 +234,9 @@ class CoverTemplate(CoverDevice):
None is unknown, 0 is closed, 100 is fully open.
"""
return self._position
if self._position_template or self._position_script:
return self._position
return None
@property
def current_cover_tilt_position(self):

View File

@@ -0,0 +1,25 @@
{
"config": {
"title": "deCONZ",
"step": {
"init": {
"title": "Define deCONZ gateway",
"data": {
"host": "Host",
"port": "Port (default value: '80')"
}
},
"link": {
"title": "Link with deCONZ",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
}
},
"error": {
"no_key": "Couldn't get an API key"
},
"abort": {
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
}
}

View File

@@ -4,22 +4,21 @@ Support for deCONZ devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/
"""
import asyncio
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
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
REQUIREMENTS = ['pydeconz==30']
REQUIREMENTS = ['pydeconz==35']
_LOGGER = logging.getLogger(__name__)
@@ -57,30 +56,28 @@ Unlock your deCONZ gateway to register with Home Assistant.
"""
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up services and configuration for deCONZ component."""
result = False
config_file = yield from hass.async_add_job(
config_file = await hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE))
@asyncio.coroutine
def async_deconz_discovered(service, discovery_info):
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)
yield from async_request_configuration(hass, config, deconz_config)
await async_request_configuration(hass, config, deconz_config)
if config_file:
result = yield from async_setup_deconz(hass, config, 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 = yield from async_setup_deconz(hass, config, deconz_config)
result = await async_setup_deconz(hass, config, deconz_config)
else:
yield from async_request_configuration(hass, config, deconz_config)
await async_request_configuration(hass, config, deconz_config)
return True
if not result:
@@ -89,8 +86,7 @@ def async_setup(hass, config):
return True
@asyncio.coroutine
def async_setup_deconz(hass, config, deconz_config):
async def async_setup_deconz(hass, config, deconz_config):
"""Set up a deCONZ session.
Load config, group, light and sensor data for server information.
@@ -100,7 +96,7 @@ def async_setup_deconz(hass, config, deconz_config):
from pydeconz import DeconzSession
websession = async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, websession, **deconz_config)
result = yield from deconz.async_load_parameters()
result = await deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ")
return False
@@ -113,8 +109,7 @@ def async_setup_deconz(hass, config, deconz_config):
hass, component, DOMAIN, {}, config))
deconz.start()
@asyncio.coroutine
def async_configure(call):
async def async_configure(call):
"""Set attribute of device in deCONZ.
Field is a string representing a specific device in deCONZ
@@ -140,7 +135,7 @@ def async_setup_deconz(hass, config, deconz_config):
if field is None:
_LOGGER.error('Could not find the entity %s', entity_id)
return
yield from deconz.async_put_state(field, data)
await deconz.async_put_state(field, data)
hass.services.async_register(
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
@@ -159,21 +154,20 @@ def async_setup_deconz(hass, config, deconz_config):
return True
@asyncio.coroutine
def async_request_configuration(hass, config, deconz_config):
async def async_request_configuration(hass, config, deconz_config):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
@asyncio.coroutine
def async_configuration_callback(data):
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
api_key = yield from async_get_api_key(hass.loop, **deconz_config)
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 = yield from async_setup_deconz(hass, config, deconz_config)
result = await async_setup_deconz(hass, config, deconz_config)
if result:
yield from hass.async_add_job(
await hass.async_add_job(
save_json, hass.config.path(CONFIG_FILE), deconz_config)
configurator.async_request_done(request_id)
return
@@ -194,3 +188,85 @@ def async_request_configuration(hass, config, deconz_config):
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

View File

@@ -0,0 +1,25 @@
{
"config": {
"title": "deCONZ",
"step": {
"init": {
"title": "Define deCONZ gateway",
"data": {
"host": "Host",
"port": "Port (default value: '80')"
}
},
"link": {
"title": "Link with deCONZ",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
}
},
"error": {
"no_key": "Couldn't get an API key"
},
"abort": {
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
}
}

View File

@@ -28,7 +28,7 @@ from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump

View File

@@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_PUB_KEY = 'pub_key'
CONF_SSH_KEY = 'ssh_key'
CONF_REQUIRE_IP = 'require_ip'
DEFAULT_SSH_PORT = 22
SECRET_GROUP = 'Password or SSH Key'
@@ -36,6 +37,7 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
@@ -115,6 +117,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE]
self.port = config[CONF_PORT]
self.require_ip = config[CONF_REQUIRE_IP]
if self.protocol == 'ssh':
self.connection = SshConnection(
@@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
ret_devices = {}
for key in devices:
if devices[key].ip is not None:
if not self.require_ip or devices[key].ip is not None:
ret_devices[key] = devices[key]
return ret_devices

View File

@@ -17,12 +17,15 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pybluez==0.22']
REQUIREMENTS = ['pybluez==0.22', 'bt_proximity==0.1.2']
BT_PREFIX = 'BT_'
CONF_REQUEST_RSSI = 'request_rssi'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TRACK_NEW): cv.boolean
vol.Optional(CONF_TRACK_NEW): cv.boolean,
vol.Optional(CONF_REQUEST_RSSI): cv.boolean
})
@@ -30,11 +33,15 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Bluetooth Scanner."""
# pylint: disable=import-error
import bluetooth
from bt_proximity import BluetoothRSSI
def see_device(device):
def see_device(mac, name, rssi=None):
"""Mark a device as seen."""
see(mac=BT_PREFIX + device[0], host_name=device[1],
source_type=SOURCE_TYPE_BLUETOOTH)
attributes = {}
if rssi is not None:
attributes['rssi'] = rssi
see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name,
attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
def discover_devices():
"""Discover Bluetooth devices."""
@@ -64,27 +71,32 @@ def setup_scanner(hass, config, see, discovery_info=None):
if track_new:
for dev in discover_devices():
if dev[0] not in devs_to_track and \
dev[0] not in devs_donot_track:
dev[0] not in devs_donot_track:
devs_to_track.append(dev[0])
see_device(dev)
see_device(dev[0], dev[1])
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
request_rssi = config.get(CONF_REQUEST_RSSI, False)
def update_bluetooth(now):
"""Lookup Bluetooth device and update status."""
try:
if track_new:
for dev in discover_devices():
if dev[0] not in devs_to_track and \
dev[0] not in devs_donot_track:
dev[0] not in devs_donot_track:
devs_to_track.append(dev[0])
for mac in devs_to_track:
_LOGGER.debug("Scanning %s", mac)
result = bluetooth.lookup_name(mac, timeout=5)
if not result:
rssi = None
if request_rssi:
rssi = BluetoothRSSI(mac).request_rssi()
if result is None:
# Could not lookup device name
continue
see_device((mac, result))
see_device(mac, result, rssi)
except bluetooth.BluetoothError:
_LOGGER.exception("Error looking up Bluetooth device")
track_point_in_utc_time(

View File

@@ -36,16 +36,20 @@ class BMWDeviceTracker(object):
self.vehicle = vehicle
def update(self) -> None:
"""Update the device info."""
dev_id = slugify(self.vehicle.modelName)
"""Update the device info.
Only update the state in home assistant if tracking in
the car is enabled.
"""
dev_id = slugify(self.vehicle.name)
if not self.vehicle.state.is_vehicle_tracking_enabled:
_LOGGER.debug('Tracking is disabled for vehicle %s', dev_id)
return
_LOGGER.debug('Updating %s', dev_id)
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': self.vehicle.modelName
}
self._see(
dev_id=dev_id, host_name=self.vehicle.modelName,
gps=self.vehicle.state.gps_position, attributes=attrs,
icon='mdi:car'
dev_id=dev_id, host_name=self.vehicle.name,
gps=self.vehicle.state.gps_position, icon='mdi:car'
)

View File

@@ -0,0 +1,83 @@
"""
Support for Google Maps location sharing.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.google_maps/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['locationsharinglib==0.4.0']
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def setup_scanner(hass, config: ConfigType, see, discovery_info=None):
"""Set up the scanner."""
scanner = GoogleMapsScanner(hass, config, see)
return scanner.success_init
class GoogleMapsScanner(object):
"""Representation of an Google Maps location sharing account."""
def __init__(self, hass, config: ConfigType, see) -> None:
"""Initialize the scanner."""
from locationsharinglib import Service
from locationsharinglib.locationsharinglibexceptions import InvalidUser
self.see = see
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
try:
self.service = Service(self.username, self.password,
hass.config.path(CREDENTIALS_FILE))
self._update_info()
track_time_interval(
hass, self._update_info, MIN_TIME_BETWEEN_SCANS)
self.success_init = True
except InvalidUser:
_LOGGER.error('You have specified invalid login credentials')
self.success_init = False
def _update_info(self, now=None):
for person in self.service.get_all_people():
dev_id = 'google_maps_{0}'.format(slugify(person.id))
attrs = {
'id': person.id,
'nickname': person.nickname,
'full_name': person.full_name,
'last_seen': person.datetime,
'address': person.address
}
self.see(
dev_id=dev_id,
gps=(person.latitude, person.longitude),
picture=person.picture_url,
source_type=SOURCE_TYPE_GPS,
attributes=attrs
)

View File

@@ -1,74 +0,0 @@
"""
Support for Mercedes cars with Mercedes ME.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/device_tracker.mercedesme/
"""
import logging
from datetime import timedelta
from homeassistant.components.mercedesme import DATA_MME
from homeassistant.helpers.event import track_time_interval
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mercedesme']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Mercedes ME tracker."""
if discovery_info is None:
return False
data = hass.data[DATA_MME].data
if not data.cars:
return False
MercedesMEDeviceTracker(hass, config, see, data)
return True
class MercedesMEDeviceTracker(object):
"""A class representing a Mercedes ME device tracker."""
def __init__(self, hass, config, see, data):
"""Initialize the Mercedes ME device tracker."""
self.see = see
self.data = data
self.update_info()
track_time_interval(
hass, self.update_info, MIN_TIME_BETWEEN_SCANS)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def update_info(self, now=None):
"""Update the device info."""
for device in self.data.cars:
if not device['services'].get('VEHICLE_FINDER', False):
continue
location = self.data.get_location(device["vin"])
if location is None:
continue
dev_id = device["vin"]
name = device["license"]
lat = location['positionLat']['value']
lon = location['positionLong']['value']
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': name
}
self.see(
dev_id=dev_id, host_name=name,
gps=(lat, lon), attributes=attrs
)
return True

View File

@@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner):
self.host,
self.username,
self.password,
port=int(self.port)
port=int(self.port),
encoding='utf-8'
)
try:

View File

@@ -6,15 +6,15 @@ https://home-assistant.io/components/device_tracker.mysensors/
"""
from homeassistant.components import mysensors
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.helpers.dispatcher import dispatcher_connect
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
def setup_scanner(hass, config, see, discovery_info=None):
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the MySensors device scanner."""
new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
device_args=(see, ))
device_args=(async_see, ))
if not new_devices:
return False
@@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None):
dev_id = (
id(device.gateway), device.node_id, device.child_id,
device.value_type)
dispatcher_connect(
async_dispatcher_connect(
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
device.update_callback)
device.async_update_callback)
return True
@@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None):
class MySensorsDeviceScanner(mysensors.MySensorsDevice):
"""Represent a MySensors scanner."""
def __init__(self, see, *args):
def __init__(self, async_see, *args):
"""Set up instance."""
super().__init__(*args)
self.see = see
self.async_see = async_see
def update_callback(self):
async def async_update_callback(self):
"""Update the device."""
self.update()
await self.async_update()
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
position = child.values[self.value_type]
latitude, longitude, _ = position.split(',')
self.see(
await self.async_see(
dev_id=slugify(self.name),
host_name=self.name,
gps=(latitude, longitude),

View File

@@ -100,7 +100,7 @@ class TadoDeviceScanner(DeviceScanner):
last_results = []
try:
with async_timeout.timeout(10, loop=self.hass.loop):
with async_timeout.timeout(10):
# Format the URL here, so we can log the template URL if
# anything goes wrong without exposing username and password.
url = self.tadoapiurl.format(

View File

@@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner):
return self.last_results
def _generate_mac2name(self):
"""Return empty MAC to name dict. Overriden if DHCP server is set."""
"""Return empty MAC to name dict. Overridden if DHCP server is set."""
self.mac2name = dict()
@_refresh_on_access_denied

View File

@@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner):
# Filter clients to provided SSID list
if self._ssid_filter:
clients = [client for client in clients
if client['essid'] in self._ssid_filter]
if 'essid' in client and
client['essid'] in self._ssid_filter]
self._clients = {
client['mac']: client

View File

@@ -0,0 +1,77 @@
"""
Support for Xiaomi Mi WiFi Repeater 2.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/device_tracker.xiaomi_miio/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
DeviceScanner)
from homeassistant.const import (CONF_HOST, CONF_TOKEN)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
})
REQUIREMENTS = ['python-miio==0.3.9']
def get_scanner(hass, config):
"""Return a Xiaomi MiIO device scanner."""
from miio import WifiRepeater, DeviceException
scanner = None
host = config[DOMAIN].get(CONF_HOST)
token = config[DOMAIN].get(CONF_TOKEN)
_LOGGER.info(
"Initializing with host %s (token %s...)", host, token[:5])
try:
device = WifiRepeater(host, token)
device_info = device.info()
_LOGGER.info("%s %s %s detected",
device_info.model,
device_info.firmware_version,
device_info.hardware_version)
scanner = XiaomiMiioDeviceScanner(hass, device)
except DeviceException as ex:
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
return scanner
class XiaomiMiioDeviceScanner(DeviceScanner):
"""This class queries a Xiaomi Mi WiFi Repeater."""
def __init__(self, hass, device):
"""Initialize the scanner."""
self.device = device
async def async_scan_devices(self):
"""Scan for devices and return a list containing found device ids."""
from miio import DeviceException
devices = []
try:
station_info = await self.hass.async_add_job(self.device.status)
_LOGGER.debug("Got new station info: %s", station_info)
for device in station_info['mat']:
devices.append(device['mac'])
except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)
return devices
async def async_get_device_name(self, device):
"""The repeater doesn't provide the name of the associated device."""
return None

View File

@@ -6,7 +6,6 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
"""
import asyncio
import json
from datetime import timedelta
import logging
@@ -14,6 +13,7 @@ import os
import voluptuous as vol
from homeassistant import config_entries
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.2.4']
REQUIREMENTS = ['netdisco==1.3.0']
DOMAIN = 'discovery'
@@ -39,6 +39,11 @@ SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
CONFIG_ENTRY_HANDLERS = {
SERVICE_HUE: 'hue',
}
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
@@ -51,9 +56,9 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),
@@ -84,8 +89,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Start a discovery service."""
from netdisco.discovery import NetworkDiscovery
@@ -99,13 +103,26 @@ def async_setup(hass, config):
# Platforms ignore by config
ignored_platforms = config[DOMAIN][CONF_IGNORE]
@asyncio.coroutine
def new_service_found(service, info):
async def new_service_found(service, info):
"""Handle a new service if one is found."""
if service in ignored_platforms:
logger.info("Ignoring service: %s %s", service, info)
return
discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered:
return
already_discovered.add(discovery_hash)
if service in CONFIG_ENTRY_HANDLERS:
await hass.config_entries.flow.async_init(
CONFIG_ENTRY_HANDLERS[service],
source=config_entries.SOURCE_DISCOVERY,
data=info
)
return
comp_plat = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service.
@@ -113,26 +130,19 @@ def async_setup(hass, config):
logger.info("Unknown service discovered: %s %s", service, info)
return
discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered:
return
already_discovered.add(discovery_hash)
logger.info("Found new service: %s %s", service, info)
component, platform = comp_plat
if platform is None:
yield from async_discover(hass, service, info, component, config)
await async_discover(hass, service, info, component, config)
else:
yield from async_load_platform(
await async_load_platform(
hass, component, platform, info, config)
@asyncio.coroutine
def scan_devices(now):
async def scan_devices(now):
"""Scan for devices."""
results = yield from hass.async_add_job(_discover, netdisco)
results = await hass.async_add_job(_discover, netdisco)
for result in results:
hass.async_add_job(new_service_found(*result))

View File

@@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.http import HomeAssistantView
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['DoorBirdPy==0.1.2']
REQUIREMENTS = ['DoorBirdPy==0.1.3']
_LOGGER = logging.getLogger(__name__)
@@ -22,6 +22,7 @@ DOMAIN = 'doorbird'
API_URL = '/api/{}'.format(DOMAIN)
CONF_DOORBELL_EVENTS = 'doorbell_events'
CONF_CUSTOM_URL = 'hass_url_override'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
vol.Optional(CONF_CUSTOM_URL): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
@@ -61,9 +63,17 @@ def setup(hass, config):
# Provide an endpoint for the device to call to trigger events
hass.http.register_view(DoorbirdRequestView())
# Get the URL of this server
hass_url = hass.config.api.base_url
# Override it if another is specified in the component configuration
if config[DOMAIN].get(CONF_CUSTOM_URL):
hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
_LOGGER.info("DoorBird will connect to this instance via %s",
hass_url)
# This will make HA the only service that gets doorbell events
url = '{}{}/{}'.format(
hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
device.reset_notifications()
device.subscribe_notification(SENSOR_DOORBELL, url)

View File

@@ -25,6 +25,8 @@ ATTR_OVERWRITE = 'overwrite'
CONF_DOWNLOAD_DIR = 'download_dir'
DOMAIN = 'downloader'
DOWNLOAD_FAILED_EVENT = 'download_failed'
DOWNLOAD_COMPLETED_EVENT = 'download_completed'
SERVICE_DOWNLOAD_FILE = 'download_file'
@@ -133,9 +135,19 @@ def setup(hass, config):
fil.write(chunk)
_LOGGER.debug("Downloading of %s done", url)
hass.bus.fire(
"{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), {
'url': url,
'filename': filename
})
except requests.exceptions.ConnectionError:
_LOGGER.exception("ConnectionError occurred for %s", url)
hass.bus.fire(
"{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), {
'url': url,
'filename': filename
})
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):

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.15']
REQUIREMENTS = ['python-ecobee-api==0.0.17']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pythonegardia==1.0.38']
REQUIREMENTS = ['pythonegardia==1.0.39']
_LOGGER = logging.getLogger(__name__)

View File

@@ -158,10 +158,6 @@ class Config(object):
"Listen port not specified, defaulting to %s",
self.listen_port)
if self.type == TYPE_GOOGLE and self.listen_port != 80:
_LOGGER.warning("When targeting Google Home, listening port has "
"to be port 80")
# Get whether or not UPNP binds to multicast address (239.255.255.250)
# or to the unicast address (host_ip_addr)
self.upnp_bind_multicast = conf.get(

View File

@@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on:
description: Turn the buzzer on.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_buzzer_off:
description: Turn the buzzer off.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_on:
description: Turn the led on.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_off:
description: Turn the led off.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_on:
description: Turn the child lock on.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_off:
description: Turn the child lock off.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_favorite_level:
description: Set the favorite level.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
level:
description: Level, between 0 and 16.
example: 1
@@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness:
description: Set the led brightness.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
brightness:
description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
example: 1
xiaomi_miio_set_auto_detect_on:
description: Turn the auto detect on.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_auto_detect_off:
description: Turn the auto detect off.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_learn_mode_on:
description: Turn the learn mode on.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_learn_mode_off:
description: Turn the learn mode off.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_volume:
description: Set the sound volume.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
volume:
description: Volume, between 0 and 100.
example: 50
xiaomi_miio_reset_filter:
description: Reset the filter lifetime and usage.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_extra_features:
description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
features:
description: Integer, known values are 0 (default) and 1 (turbo mode).
example: 1
xiaomi_miio_set_target_humidity:
description: Set the target humidity.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
humidity:
description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80.
example: 50
xiaomi_miio_set_dry_on:
description: Turn the dry mode on.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_dry_off:
description: Turn the dry mode off.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'

View File

@@ -1,16 +1,16 @@
"""
Support for Xiaomi Mi Air Purifier 2.
Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.xiaomi_miio/
"""
import asyncio
from enum import Enum
from functools import partial
import logging
import voluptuous as vol
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
SUPPORT_SET_SPEED, DOMAIN, )
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
@@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Air Purifier'
PLATFORM = 'xiaomi_miio'
DEFAULT_NAME = 'Xiaomi Miio Device'
DATA_KEY = 'fan.xiaomi_miio'
CONF_MODEL = 'model'
MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6'
MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3'
MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MODEL): vol.In(
['zhimi.airpurifier.m1',
'zhimi.airpurifier.m2',
'zhimi.airpurifier.ma1',
'zhimi.airpurifier.ma2',
'zhimi.airpurifier.sa1',
'zhimi.airpurifier.sa2',
'zhimi.airpurifier.v1',
'zhimi.airpurifier.v2',
'zhimi.airpurifier.v3',
'zhimi.airpurifier.v5',
'zhimi.airpurifier.v6',
'zhimi.humidifier.v1',
'zhimi.humidifier.ca1']),
})
REQUIREMENTS = ['python-miio==0.3.7']
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
ATTR_MODEL = 'model'
# Air Purifier
ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity'
ATTR_AIR_QUALITY_INDEX = 'aqi'
@@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness'
ATTR_MOTOR_SPEED = 'motor_speed'
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
ATTR_PURIFY_VOLUME = 'purify_volume'
ATTR_BRIGHTNESS = 'brightness'
ATTR_LEVEL = 'level'
ATTR_MOTOR2_SPEED = 'motor2_speed'
ATTR_ILLUMINANCE = 'illuminance'
ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id'
ATTR_FILTER_RFID_TAG = 'filter_rfid_tag'
ATTR_FILTER_TYPE = 'filter_type'
ATTR_LEARN_MODE = 'learn_mode'
ATTR_SLEEP_TIME = 'sleep_time'
ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count'
ATTR_EXTRA_FEATURES = 'extra_features'
ATTR_FEATURES = 'features'
ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported'
ATTR_AUTO_DETECT = 'auto_detect'
ATTR_SLEEP_MODE = 'sleep_mode'
ATTR_VOLUME = 'volume'
ATTR_USE_TIME = 'use_time'
ATTR_BUTTON_PRESSED = 'button_pressed'
# Air Humidifier
ATTR_TARGET_HUMIDITY = 'target_humidity'
ATTR_TRANS_LEVEL = 'trans_level'
ATTR_HARDWARE_VERSION = 'hardware_version'
# Air Humidifier CA
ATTR_SPEED = 'speed'
ATTR_DEPTH = 'depth'
ATTR_DRY = 'dry'
# Map attributes to properties of the state object
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
ATTR_TEMPERATURE: 'temperature',
ATTR_HUMIDITY: 'humidity',
ATTR_AIR_QUALITY_INDEX: 'aqi',
ATTR_MODE: 'mode',
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
ATTR_FILTER_LIFE: 'filter_life_remaining',
ATTR_FAVORITE_LEVEL: 'favorite_level',
ATTR_CHILD_LOCK: 'child_lock',
ATTR_LED: 'led',
ATTR_MOTOR_SPEED: 'motor_speed',
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
ATTR_PURIFY_VOLUME: 'purify_volume',
ATTR_LEARN_MODE: 'learn_mode',
ATTR_SLEEP_TIME: 'sleep_time',
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
ATTR_EXTRA_FEATURES: 'extra_features',
ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported',
ATTR_AUTO_DETECT: 'auto_detect',
ATTR_USE_TIME: 'use_time',
ATTR_BUTTON_PRESSED: 'button_pressed',
}
AVAILABLE_ATTRIBUTES_AIRPURIFIER = {
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
ATTR_BUZZER: 'buzzer',
ATTR_LED_BRIGHTNESS: 'led_brightness',
ATTR_SLEEP_MODE: 'sleep_mode',
}
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = {
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
ATTR_FILTER_TYPE: 'filter_type',
ATTR_ILLUMINANCE: 'illuminance',
ATTR_MOTOR2_SPEED: 'motor2_speed',
ATTR_VOLUME: 'volume',
}
AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
# Common set isn't used here. It's a very basic version of the device.
ATTR_AIR_QUALITY_INDEX: 'aqi',
ATTR_MODE: 'mode',
ATTR_LED: 'led',
ATTR_BUZZER: 'buzzer',
ATTR_CHILD_LOCK: 'child_lock',
ATTR_ILLUMINANCE: 'illuminance',
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
ATTR_FILTER_LIFE: 'filter_life_remaining',
ATTR_MOTOR_SPEED: 'motor_speed',
# perhaps supported but unconfirmed
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
ATTR_VOLUME: 'volume',
ATTR_MOTOR2_SPEED: 'motor2_speed',
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
ATTR_FILTER_TYPE: 'filter_type',
ATTR_PURIFY_VOLUME: 'purify_volume',
ATTR_LEARN_MODE: 'learn_mode',
ATTR_SLEEP_TIME: 'sleep_time',
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
ATTR_EXTRA_FEATURES: 'extra_features',
ATTR_AUTO_DETECT: 'auto_detect',
ATTR_USE_TIME: 'use_time',
ATTR_BUTTON_PRESSED: 'button_pressed',
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
ATTR_TEMPERATURE: 'temperature',
ATTR_HUMIDITY: 'humidity',
ATTR_MODE: 'mode',
ATTR_BUZZER: 'buzzer',
ATTR_CHILD_LOCK: 'child_lock',
ATTR_TRANS_LEVEL: 'trans_level',
ATTR_TARGET_HUMIDITY: 'target_humidity',
ATTR_LED_BRIGHTNESS: 'led_brightness',
ATTR_BUTTON_PRESSED: 'button_pressed',
ATTR_USE_TIME: 'use_time',
ATTR_HARDWARE_VERSION: 'hardware_version',
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = {
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER,
ATTR_SPEED: 'speed',
ATTR_DEPTH: 'depth',
ATTR_DRY: 'dry',
}
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
'Medium', 'High', 'Strong']
SUCCESS = ['ok']
FEATURE_SET_BUZZER = 1
FEATURE_SET_LED = 2
FEATURE_SET_CHILD_LOCK = 4
FEATURE_SET_LED_BRIGHTNESS = 8
FEATURE_SET_FAVORITE_LEVEL = 16
FEATURE_SET_AUTO_DETECT = 32
FEATURE_SET_LEARN_MODE = 64
FEATURE_SET_VOLUME = 128
FEATURE_RESET_FILTER = 256
FEATURE_SET_EXTRA_FEATURES = 512
FEATURE_SET_TARGET_HUMIDITY = 1024
FEATURE_SET_DRY = 2048
FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER |
FEATURE_SET_CHILD_LOCK)
FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC |
FEATURE_SET_LED |
FEATURE_SET_LED_BRIGHTNESS |
FEATURE_SET_FAVORITE_LEVEL |
FEATURE_SET_LEARN_MODE |
FEATURE_RESET_FILTER |
FEATURE_SET_EXTRA_FEATURES)
FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK |
FEATURE_SET_LED |
FEATURE_SET_FAVORITE_LEVEL |
FEATURE_SET_AUTO_DETECT |
FEATURE_SET_VOLUME)
FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC |
FEATURE_SET_LED)
FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC |
FEATURE_SET_LED_BRIGHTNESS |
FEATURE_SET_TARGET_HUMIDITY)
FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER |
FEATURE_SET_DRY)
SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on'
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off'
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness'
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on'
SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off'
SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on'
SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off'
SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume'
SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter'
SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features'
SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity'
SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on'
SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off'
AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
@@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16))
})
SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_VOLUME):
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
})
SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FEATURES):
vol.All(vol.Coerce(int), vol.Range(min=0))
})
SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_HUMIDITY):
vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80]))
})
SERVICE_TO_METHOD = {
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'},
@@ -81,59 +289,99 @@ SERVICE_TO_METHOD = {
SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'},
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
SERVICE_SET_FAVORITE_LEVEL: {
'method': 'async_set_favorite_level',
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'},
SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'},
SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'},
SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'},
SERVICE_RESET_FILTER: {'method': 'async_reset_filter'},
SERVICE_SET_LED_BRIGHTNESS: {
'method': 'async_set_led_brightness',
'schema': SERVICE_SCHEMA_LED_BRIGHTNESS},
SERVICE_SET_FAVORITE_LEVEL: {
'method': 'async_set_favorite_level',
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
SERVICE_SET_VOLUME: {
'method': 'async_set_volume',
'schema': SERVICE_SCHEMA_VOLUME},
SERVICE_SET_EXTRA_FEATURES: {
'method': 'async_set_extra_features',
'schema': SERVICE_SCHEMA_EXTRA_FEATURES},
SERVICE_SET_TARGET_HUMIDITY: {
'method': 'async_set_target_humidity',
'schema': SERVICE_SCHEMA_TARGET_HUMIDITY},
SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'},
SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'},
}
# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the air purifier from config."""
from miio import AirPurifier, DeviceException
if PLATFORM not in hass.data:
hass.data[PLATFORM] = {}
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the miio fan device from config."""
from miio import Device, DeviceException
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN)
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
unique_id = None
try:
if model is None:
try:
miio_device = Device(host, token)
device_info = miio_device.info()
model = device_info.model
unique_id = "{}-{}".format(model, device_info.mac_address)
_LOGGER.info("%s %s %s detected",
model,
device_info.firmware_version,
device_info.hardware_version)
except DeviceException:
raise PlatformNotReady
if model.startswith('zhimi.airpurifier.'):
from miio import AirPurifier
air_purifier = AirPurifier(host, token)
device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
elif model.startswith('zhimi.humidifier.'):
from miio import AirHumidifier
air_humidifier = AirHumidifier(host, token)
device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
else:
_LOGGER.error(
'Unsupported device found! Please create an issue at '
'https://github.com/syssi/xiaomi_airpurifier/issues '
'and provide the following data: %s', model)
return False
xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier)
hass.data[PLATFORM][host] = xiaomi_air_purifier
except DeviceException:
raise PlatformNotReady
hass.data[DATA_KEY][host] = device
async_add_devices([device], update_before_add=True)
async_add_devices([xiaomi_air_purifier], update_before_add=True)
@asyncio.coroutine
def async_service_handler(service):
async def async_service_handler(service):
"""Map services to methods on XiaomiAirPurifier."""
method = SERVICE_TO_METHOD.get(service.service)
params = {key: value for key, value in service.data.items()
if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
devices = [device for device in hass.data[PLATFORM].values() if
devices = [device for device in hass.data[DATA_KEY].values() if
device.entity_id in entity_ids]
else:
devices = hass.data[PLATFORM].values()
devices = hass.data[DATA_KEY].values()
update_tasks = []
for device in devices:
yield from getattr(device, method['method'])(**params)
if not hasattr(device, method['method']):
continue
await getattr(device, method['method'])(**params)
update_tasks.append(device.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
for air_purifier_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[air_purifier_service].get(
@@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
DOMAIN, air_purifier_service, async_service_handler, schema=schema)
class XiaomiAirPurifier(FanEntity):
"""Representation of a Xiaomi Air Purifier."""
class XiaomiGenericDevice(FanEntity):
"""Representation of a generic Xiaomi device."""
def __init__(self, name, air_purifier):
"""Initialize the air purifier."""
def __init__(self, name, device, model, unique_id):
"""Initialize the generic Xiaomi device."""
self._name = name
self._device = device
self._model = model
self._unique_id = unique_id
self._air_purifier = air_purifier
self._available = False
self._state = None
self._state_attrs = {
ATTR_AIR_QUALITY_INDEX: None,
ATTR_TEMPERATURE: None,
ATTR_HUMIDITY: None,
ATTR_MODE: None,
ATTR_FILTER_HOURS_USED: None,
ATTR_FILTER_LIFE: None,
ATTR_FAVORITE_LEVEL: None,
ATTR_BUZZER: None,
ATTR_CHILD_LOCK: None,
ATTR_LED: None,
ATTR_LED_BRIGHTNESS: None,
ATTR_MOTOR_SPEED: None,
ATTR_AVERAGE_AIR_QUALITY_INDEX: None,
ATTR_PURIFY_VOLUME: None,
ATTR_MODEL: self._model,
}
self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False
@property
@@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity):
@property
def should_poll(self):
"""Poll the fan."""
"""Poll the device."""
return True
@property
def unique_id(self):
"""Return an unique ID."""
return self._unique_id
@property
def name(self):
"""Return the name of the device if any."""
@@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity):
@property
def available(self):
"""Return true when state is known."""
return self._state is not None
return self._available
@property
def device_state_attributes(self):
@@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity):
@property
def is_on(self):
"""Return true if fan is on."""
"""Return true if device is on."""
return self._state
@asyncio.coroutine
def _try_command(self, mask_error, func, *args, **kwargs):
"""Call an air purifier command handling error messages."""
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
async def _try_command(self, mask_error, func, *args, **kwargs):
"""Call a miio device command handling error messages."""
from miio import DeviceException
try:
result = yield from self.hass.async_add_job(
result = await self.hass.async_add_job(
partial(func, *args, **kwargs))
_LOGGER.debug("Response received from air purifier: %s", result)
_LOGGER.debug("Response received from miio device: %s", result)
return result == SUCCESS
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
self._available = False
return False
@asyncio.coroutine
def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
"""Turn the fan on."""
async def async_turn_on(self, speed: str = None,
**kwargs) -> None:
"""Turn the device on."""
if speed:
# If operation mode was set the device must not be turned on.
result = yield from self.async_set_speed(speed)
result = await self.async_set_speed(speed)
else:
result = yield from self._try_command(
"Turning the air purifier on failed.", self._air_purifier.on)
result = await self._try_command(
"Turning the miio device on failed.", self._device.on)
if result:
self._state = True
self._skip_update = True
@asyncio.coroutine
def async_turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn the fan off."""
result = yield from self._try_command(
"Turning the air purifier off failed.", self._air_purifier.off)
async def async_turn_off(self, **kwargs) -> None:
"""Turn the device off."""
result = await self._try_command(
"Turning the miio device off failed.", self._device.off)
if result:
self._state = False
self._skip_update = True
@asyncio.coroutine
def async_update(self):
async def async_set_buzzer_on(self):
"""Turn the buzzer on."""
if self._device_features & FEATURE_SET_BUZZER == 0:
return
await self._try_command(
"Turning the buzzer of the miio device on failed.",
self._device.set_buzzer, True)
async def async_set_buzzer_off(self):
"""Turn the buzzer off."""
if self._device_features & FEATURE_SET_BUZZER == 0:
return
await self._try_command(
"Turning the buzzer of the miio device off failed.",
self._device.set_buzzer, False)
async def async_set_child_lock_on(self):
"""Turn the child lock on."""
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
return
await self._try_command(
"Turning the child lock of the miio device on failed.",
self._device.set_child_lock, True)
async def async_set_child_lock_off(self):
"""Turn the child lock off."""
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
return
await self._try_command(
"Turning the child lock of the miio device off failed.",
self._device.set_child_lock, False)
class XiaomiAirPurifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Purifier."""
def __init__(self, name, device, model, unique_id):
"""Initialize the plug switch."""
super().__init__(name, device, model, unique_id)
if self._model == MODEL_AIRPURIFIER_PRO:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO
elif self._model == MODEL_AIRPURIFIER_V3:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
self._speed_list = OPERATION_MODES_AIRPURIFIER_V3
else:
self._device_features = FEATURE_FLAGS_AIRPURIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER
self._speed_list = OPERATION_MODES_AIRPURIFIER
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes})
async def async_update(self):
"""Fetch state from the device."""
from miio import DeviceException
@@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity):
return
try:
state = yield from self.hass.async_add_job(
self._air_purifier.status)
state = await self.hass.async_add_job(
self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._state_attrs = {
ATTR_TEMPERATURE: state.temperature,
ATTR_HUMIDITY: state.humidity,
ATTR_AIR_QUALITY_INDEX: state.aqi,
ATTR_MODE: state.mode.value,
ATTR_FILTER_HOURS_USED: state.filter_hours_used,
ATTR_FILTER_LIFE: state.filter_life_remaining,
ATTR_FAVORITE_LEVEL: state.favorite_level,
ATTR_BUZZER: state.buzzer,
ATTR_CHILD_LOCK: state.child_lock,
ATTR_LED: state.led,
ATTR_MOTOR_SPEED: state.motor_speed,
ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi,
ATTR_PURIFY_VOLUME: state.purify_volume,
}
if state.led_brightness:
self._state_attrs[
ATTR_LED_BRIGHTNESS] = state.led_brightness.value
self._state_attrs.update(
{key: self._extract_value_from_attribute(state, value) for
key, value in self._available_attributes.items()})
except DeviceException as ex:
self._state = None
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def speed_list(self: ToggleEntity) -> list:
def speed_list(self) -> list:
"""Get the list of available speeds."""
from miio.airpurifier import OperationMode
return [mode.name for mode in OperationMode]
return self._speed_list
@property
def speed(self):
@@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity):
return None
@asyncio.coroutine
def async_set_speed(self: ToggleEntity, speed: str) -> None:
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
_LOGGER.debug("Setting the operation mode to: %s", speed)
if self.supported_features & SUPPORT_SET_SPEED == 0:
return
from miio.airpurifier import OperationMode
yield from self._try_command(
"Setting operation mode of the air purifier failed.",
self._air_purifier.set_mode, OperationMode[speed.title()])
_LOGGER.debug("Setting the operation mode to: %s", speed)
@asyncio.coroutine
def async_set_buzzer_on(self):
"""Turn the buzzer on."""
yield from self._try_command(
"Turning the buzzer of the air purifier on failed.",
self._air_purifier.set_buzzer, True)
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode, OperationMode[speed.title()])
@asyncio.coroutine
def async_set_buzzer_off(self):
"""Turn the buzzer off."""
yield from self._try_command(
"Turning the buzzer of the air purifier off failed.",
self._air_purifier.set_buzzer, False)
@asyncio.coroutine
def async_set_led_on(self):
async def async_set_led_on(self):
"""Turn the led on."""
yield from self._try_command(
"Turning the led of the air purifier off failed.",
self._air_purifier.set_led, True)
if self._device_features & FEATURE_SET_LED == 0:
return
@asyncio.coroutine
def async_set_led_off(self):
await self._try_command(
"Turning the led of the miio device off failed.",
self._device.set_led, True)
async def async_set_led_off(self):
"""Turn the led off."""
yield from self._try_command(
"Turning the led of the air purifier off failed.",
self._air_purifier.set_led, False)
if self._device_features & FEATURE_SET_LED == 0:
return
@asyncio.coroutine
def async_set_child_lock_on(self):
"""Turn the child lock on."""
yield from self._try_command(
"Turning the child lock of the air purifier on failed.",
self._air_purifier.set_child_lock, True)
await self._try_command(
"Turning the led of the miio device off failed.",
self._device.set_led, False)
@asyncio.coroutine
def async_set_child_lock_off(self):
"""Turn the child lock off."""
yield from self._try_command(
"Turning the child lock of the air purifier off failed.",
self._air_purifier.set_child_lock, False)
@asyncio.coroutine
def async_set_led_brightness(self, brightness: int = 2):
async def async_set_led_brightness(self, brightness: int = 2):
"""Set the led brightness."""
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
return
from miio.airpurifier import LedBrightness
yield from self._try_command(
"Setting the led brightness of the air purifier failed.",
self._air_purifier.set_led_brightness, LedBrightness(brightness))
await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness, LedBrightness(brightness))
@asyncio.coroutine
def async_set_favorite_level(self, level: int = 1):
async def async_set_favorite_level(self, level: int = 1):
"""Set the favorite level."""
yield from self._try_command(
"Setting the favorite level of the air purifier failed.",
self._air_purifier.set_favorite_level, level)
if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0:
return
await self._try_command(
"Setting the favorite level of the miio device failed.",
self._device.set_favorite_level, level)
async def async_set_auto_detect_on(self):
"""Turn the auto detect on."""
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
return
await self._try_command(
"Turning the auto detect of the miio device on failed.",
self._device.set_auto_detect, True)
async def async_set_auto_detect_off(self):
"""Turn the auto detect off."""
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
return
await self._try_command(
"Turning the auto detect of the miio device off failed.",
self._device.set_auto_detect, False)
async def async_set_learn_mode_on(self):
"""Turn the learn mode on."""
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
return
await self._try_command(
"Turning the learn mode of the miio device on failed.",
self._device.set_learn_mode, True)
async def async_set_learn_mode_off(self):
"""Turn the learn mode off."""
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
return
await self._try_command(
"Turning the learn mode of the miio device off failed.",
self._device.set_learn_mode, False)
async def async_set_volume(self, volume: int = 50):
"""Set the sound volume."""
if self._device_features & FEATURE_SET_VOLUME == 0:
return
await self._try_command(
"Setting the sound volume of the miio device failed.",
self._device.set_volume, volume)
async def async_set_extra_features(self, features: int = 1):
"""Set the extra features."""
if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0:
return
await self._try_command(
"Setting the extra features of the miio device failed.",
self._device.set_extra_features, features)
async def async_reset_filter(self):
"""Reset the filter lifetime and usage."""
if self._device_features & FEATURE_RESET_FILTER == 0:
return
await self._try_command(
"Resetting the filter lifetime of the miio device failed.",
self._device.reset_filter)
class XiaomiAirHumidifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Humidifier."""
def __init__(self, name, device, model, unique_id):
"""Initialize the plug switch."""
from miio.airpurifier import OperationMode
super().__init__(name, device, model, unique_id)
if self._model == MODEL_AIRHUMIDIFIER_CA:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA
self._speed_list = [mode.name for mode in OperationMode]
else:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
self._speed_list = [mode.name for mode in OperationMode if
mode.name != 'Auto']
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes})
async def async_update(self):
"""Fetch state from the device."""
from miio import DeviceException
# On state change the device doesn't provide the new state immediately.
if self._skip_update:
self._skip_update = False
return
try:
state = await self.hass.async_add_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._state_attrs.update(
{key: self._extract_value_from_attribute(state, value) for
key, value in self._available_attributes.items()})
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
def speed_list(self) -> list:
"""Get the list of available speeds."""
return self._speed_list
@property
def speed(self):
"""Return the current speed."""
if self._state:
from miio.airhumidifier import OperationMode
return OperationMode(self._state_attrs[ATTR_MODE]).name
return None
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self.supported_features & SUPPORT_SET_SPEED == 0:
return
from miio.airhumidifier import OperationMode
_LOGGER.debug("Setting the operation mode to: %s", speed)
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode, OperationMode[speed.title()])
async def async_set_led_brightness(self, brightness: int = 2):
"""Set the led brightness."""
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
return
from miio.airhumidifier import LedBrightness
await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness, LedBrightness(brightness))
async def async_set_target_humidity(self, humidity: int = 40):
"""Set the target humidity."""
if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0:
return
await self._try_command(
"Setting the target humidity of the miio device failed.",
self._device.set_target_humidity, humidity)
async def async_set_dry_on(self):
"""Turn the dry mode on."""
if self._device_features & FEATURE_SET_DRY == 0:
return
await self._try_command(
"Turning the dry mode of the miio device off failed.",
self._device.set_dry, True)
async def async_set_dry_off(self):
"""Turn the dry mode off."""
if self._device_features & FEATURE_SET_DRY == 0:
return
await self._try_command(
"Turning the dry mode of the miio device off failed.",
self._device.set_dry, False)

View File

@@ -0,0 +1,114 @@
"""
Fans on Zigbee Home Automation networks.
For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/fan.zha/
"""
import asyncio
import logging
from homeassistant.components import zha
from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_UNKNOWN
DEPENDENCIES = ['zha']
_LOGGER = logging.getLogger(__name__)
# Additional speeds in zigbee's ZCL
# Spec is unclear as to what this value means. On King Of Fans HBUniversal
# receiver, this means Very High.
SPEED_ON = 'on'
# The fan speed is self-regulated
SPEED_AUTO = 'auto'
# When the heated/cooled space is occupied, the fan is always on
SPEED_SMART = 'smart'
SPEED_LIST = [
SPEED_OFF,
SPEED_LOW,
SPEED_MEDIUM,
SPEED_HIGH,
SPEED_ON,
SPEED_AUTO,
SPEED_SMART
]
VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)}
SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Zigbee Home Automation fans."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
async_add_devices([ZhaFan(**discovery_info)], update_before_add=True)
class ZhaFan(zha.Entity, FanEntity):
"""Representation of a ZHA fan."""
_domain = DOMAIN
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return SPEED_LIST
@property
def speed(self) -> str:
"""Return the current speed."""
return self._state
@property
def is_on(self) -> bool:
"""Return true if entity is on."""
if self._state == STATE_UNKNOWN:
return False
return self._state != SPEED_OFF
@asyncio.coroutine
def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the entity on."""
if speed is None:
speed = SPEED_MEDIUM
yield from self.async_set_speed(speed)
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
yield from self.async_set_speed(SPEED_OFF)
@asyncio.coroutine
def async_set_speed(self: FanEntity, speed: str) -> None:
"""Set the speed of the fan."""
yield from self._endpoint.fan.write_attributes({
'fan_mode': SPEED_TO_VALUE[speed]})
self._state = speed
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN)
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return False

View File

@@ -0,0 +1,110 @@
"""
Component for monitoring activity on a folder.
For more details about this platform, refer to the documentation at
https://home-assistant.io/components/folder_watcher/
"""
import os
import logging
import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['watchdog==0.8.3']
_LOGGER = logging.getLogger(__name__)
CONF_FOLDER = 'folder'
CONF_PATTERNS = 'patterns'
DEFAULT_PATTERN = '*'
DOMAIN = "folder_watcher"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_FOLDER): cv.isdir,
vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]):
vol.All(cv.ensure_list, [cv.string]),
})])
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the folder watcher."""
conf = config[DOMAIN]
for watcher in conf:
path = watcher[CONF_FOLDER]
patterns = watcher[CONF_PATTERNS]
if not hass.config.is_allowed_path(path):
_LOGGER.error("folder %s is not valid or allowed", path)
return False
Watcher(path, patterns, hass)
return True
def create_event_handler(patterns, hass):
""""Return the Watchdog EventHandler object."""
from watchdog.events import PatternMatchingEventHandler
class EventHandler(PatternMatchingEventHandler):
"""Class for handling Watcher events."""
def __init__(self, patterns, hass):
"""Initialise the EventHandler."""
super().__init__(patterns)
self.hass = hass
def process(self, event):
"""On Watcher event, fire HA event."""
_LOGGER.debug("process(%s)", event)
if not event.is_directory:
folder, file_name = os.path.split(event.src_path)
self.hass.bus.fire(
DOMAIN, {
"event_type": event.event_type,
'path': event.src_path,
'file': file_name,
'folder': folder,
})
def on_modified(self, event):
"""File modified."""
self.process(event)
def on_moved(self, event):
"""File moved."""
self.process(event)
def on_created(self, event):
"""File created."""
self.process(event)
def on_deleted(self, event):
"""File deleted."""
self.process(event)
return EventHandler(patterns, hass)
class Watcher():
"""Class for starting Watchdog."""
def __init__(self, path, patterns, hass):
"""Initialise the watchdog observer."""
from watchdog.observers import Observer
self._observer = Observer()
self._observer.schedule(
create_event_handler(patterns, hass),
path,
recursive=True)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
def startup(self, event):
"""Start the watcher."""
self._observer.start()
def shutdown(self, event):
"""Shutdown the watcher."""
self._observer.stop()
self._observer.join()

View File

@@ -0,0 +1,103 @@
"""
Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/freedns/
"""
import asyncio
from datetime import timedelta
import logging
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'freedns'
DEFAULT_INTERVAL = timedelta(minutes=10)
TIMEOUT = 10
UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php'
CONF_UPDATE_INTERVAL = 'update_interval'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Exclusive(CONF_URL, DOMAIN): cv.string,
vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string,
vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta),
})
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
"""Initialize the FreeDNS component."""
url = config[DOMAIN].get(CONF_URL)
auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL)
session = hass.helpers.aiohttp_client.async_get_clientsession()
result = yield from _update_freedns(
hass, session, url, auth_token)
if result is False:
return False
@asyncio.coroutine
def update_domain_callback(now):
"""Update the FreeDNS entry."""
yield from _update_freedns(hass, session, url, auth_token)
hass.helpers.event.async_track_time_interval(
update_domain_callback, update_interval)
return True
@asyncio.coroutine
def _update_freedns(hass, session, url, auth_token):
"""Update FreeDNS."""
params = None
if url is None:
url = UPDATE_URL
if auth_token is not None:
params = {}
params[auth_token] = ""
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
resp = yield from session.get(url, params=params)
body = yield from resp.text()
if "has not changed" in body:
# IP has not changed.
_LOGGER.debug("FreeDNS update skipped: IP has not changed")
return True
if "ERROR" not in body:
_LOGGER.debug("Updating FreeDNS was successful: %s", body)
return True
if "Invalid update URL" in body:
_LOGGER.error("FreeDNS update token is invalid")
else:
_LOGGER.warning("Updating FreeDNS failed: %s", body)
except aiohttp.ClientError:
_LOGGER.warning("Can't connect to FreeDNS API")
except asyncio.TimeoutError:
_LOGGER.warning("Timeout from FreeDNS API at %s", url)
return False

View File

@@ -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==20180310.0']
REQUIREMENTS = ['home-assistant-frontend==20180404.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

View File

@@ -17,7 +17,16 @@ from homeassistant.core import callback
from homeassistant.const import (
CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES)
from homeassistant.components import (
switch, light, cover, media_player, group, fan, scene, script, climate,
climate,
cover,
fan,
group,
input_boolean,
light,
media_player,
scene,
script,
switch,
)
from . import trait
@@ -33,15 +42,16 @@ HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
DOMAIN_TO_GOOGLE_TYPES = {
climate.DOMAIN: TYPE_THERMOSTAT,
cover.DOMAIN: TYPE_SWITCH,
fan.DOMAIN: TYPE_SWITCH,
group.DOMAIN: TYPE_SWITCH,
input_boolean.DOMAIN: TYPE_SWITCH,
light.DOMAIN: TYPE_LIGHT,
media_player.DOMAIN: TYPE_SWITCH,
scene.DOMAIN: TYPE_SCENE,
script.DOMAIN: TYPE_SCENE,
switch.DOMAIN: TYPE_SWITCH,
fan.DOMAIN: TYPE_SWITCH,
light.DOMAIN: TYPE_LIGHT,
cover.DOMAIN: TYPE_SWITCH,
media_player.DOMAIN: TYPE_SWITCH,
climate.DOMAIN: TYPE_THERMOSTAT,
}
@@ -84,9 +94,16 @@ class _GoogleEntity:
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""
traits = self.traits()
state = self.state
# When a state is unavailable, the attributes that describe
# capabilities will be stripped. For example, a light entity will miss
# the min/max mireds. Therefore they will be excluded from a sync.
if state.state == STATE_UNAVAILABLE:
return None
traits = self.traits()
# Found no supported traits for this entity
if not traits:
return None

View File

@@ -243,7 +243,7 @@ class ColorSpectrumTrait(_Trait):
if domain != light.DOMAIN:
return False
return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR)
return features & light.SUPPORT_COLOR
def sync_attributes(self):
"""Return color spectrum attributes for a sync request."""
@@ -254,13 +254,11 @@ class ColorSpectrumTrait(_Trait):
"""Return color spectrum query attributes."""
response = {}
# No need to handle XY color because light component will always
# convert XY to RGB if possible (which is when brightness is available)
color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR)
if color_rgb is not None:
color_hs = self.state.attributes.get(light.ATTR_HS_COLOR)
if color_hs is not None:
response['color'] = {
'spectrumRGB': int(color_util.color_rgb_to_hex(
color_rgb[0], color_rgb[1], color_rgb[2]), 16),
*color_util.color_hs_to_RGB(*color_hs)), 16),
}
return response
@@ -274,11 +272,12 @@ class ColorSpectrumTrait(_Trait):
"""Execute a color spectrum command."""
# Convert integer to hex format and left pad with 0's till length 6
hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
color = color_util.rgb_hex_to_rgb_list(hex_value)
color = color_util.color_RGB_to_hs(
*color_util.rgb_hex_to_rgb_list(hex_value))
await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_RGB_COLOR: color
light.ATTR_HS_COLOR: color
}, blocking=True)

View File

@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.util.async_ import run_coroutine_threadsafe
DOMAIN = 'group'

View File

@@ -156,7 +156,7 @@ def async_setup(hass, config):
if 'frontend' in hass.config.components:
yield from hass.components.frontend.async_register_built_in_panel(
'hassio', 'Hass.io', 'mdi:access-point-network')
'hassio', 'Hass.io', 'mdi:home-assistant')
if 'http' in config:
yield from hassio.update_hass_api(config['http'])

View File

@@ -239,15 +239,16 @@ def get_state(hass, utc_point_in_time, entity_id, run=None):
def async_setup(hass, config):
"""Set up the history hooks."""
filters = Filters()
exclude = config[DOMAIN].get(CONF_EXCLUDE)
conf = config.get(DOMAIN, {})
exclude = conf.get(CONF_EXCLUDE)
if exclude:
filters.excluded_entities = exclude.get(CONF_ENTITIES, [])
filters.excluded_domains = exclude.get(CONF_DOMAINS, [])
include = config[DOMAIN].get(CONF_INCLUDE)
include = conf.get(CONF_INCLUDE)
if include:
filters.included_entities = include.get(CONF_ENTITIES, [])
filters.included_domains = include.get(CONF_DOMAINS, [])
use_include_order = config[DOMAIN].get(CONF_ORDER)
use_include_order = conf.get(CONF_ORDER)
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
yield from hass.components.frontend.async_register_built_in_panel(
@@ -308,7 +309,7 @@ class HistoryPeriodView(HomeAssistantView):
result = yield from hass.async_add_job(
get_significant_states, hass, start_time, end_time,
entity_ids, self.filters, include_start_time_state)
result = result.values()
result = list(result.values())
if _LOGGER.isEnabledFor(logging.DEBUG):
elapsed = time.perf_counter() - timer_start
_LOGGER.debug(
@@ -318,7 +319,6 @@ class HistoryPeriodView(HomeAssistantView):
# by any entities explicitly included in the configuration.
if self.use_include_order:
result = list(result)
sorted_result = []
for order_entity in self.filters.included_entities:
for state_list in result:

View File

@@ -3,154 +3,202 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/homekit/
"""
import asyncio
import logging
import re
from zlib import adler32
import voluptuous as vol
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.cover import 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)
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)
from .util import (
validate_entity_config, show_setup_message)
TYPES = Registry()
_LOGGER = logging.getLogger(__name__)
_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$"
DOMAIN = 'homekit'
REQUIREMENTS = ['HAP-python==1.1.7']
BRIDGE_NAME = 'Home Assistant'
CONF_PIN_CODE = 'pincode'
HOMEKIT_FILE = '.homekit.state'
def valid_pin(value):
"""Validate pin code value."""
match = re.match(_RE_VALID_PINCODE, str(value).strip())
if not match:
raise vol.Invalid("Pin must be in the format: '123-45-678'")
return match.group(0)
REQUIREMENTS = ['HAP-python==1.1.8']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All({
vol.Optional(CONF_PORT, default=51826): vol.Coerce(int),
vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
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,
})
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Setup the HomeKit component."""
_LOGGER.debug("Begin setup HomeKit")
_LOGGER.debug('Begin setup HomeKit')
conf = config[DOMAIN]
port = conf.get(CONF_PORT)
pin = str.encode(conf.get(CONF_PIN_CODE))
port = conf[CONF_PORT]
auto_start = conf[CONF_AUTO_START]
entity_filter = conf[CONF_FILTER]
entity_config = conf[CONF_ENTITY_CONFIG]
homekit = HomeKit(hass, port)
homekit.setup_bridge(pin)
homekit = HomeKit(hass, port, entity_filter, entity_config)
homekit.setup()
if auto_start:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
return True
def handle_homekit_service_start(service):
"""Handle start HomeKit service call."""
if homekit.started:
_LOGGER.warning('HomeKit is already running')
return
homekit.start()
hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START,
handle_homekit_service_start)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, homekit.start_driver)
return True
def import_types():
"""Import all types from files in the HomeKit directory."""
_LOGGER.debug("Import type files.")
# pylint: disable=unused-variable
from . import ( # noqa F401
covers, security_systems, sensors, switches, thermostats)
def get_accessory(hass, state):
def get_accessory(hass, state, aid, config):
"""Take state and return an accessory object if supported."""
if not aid:
_LOGGER.warning('The entitiy "%s" is not supported, since it '
'generates an invalid aid, please change it.',
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\"",
_LOGGER.debug('Add "%s" as "%s"',
state.entity_id, 'TemperatureSensor')
return TYPES['TemperatureSensor'](hass, state.entity_id,
state.name)
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)
elif state.domain == 'cover':
# Only add covers that support set_cover_position
if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4:
_LOGGER.debug("Add \"%s\" as \"%s\"",
state.entity_id, 'Window')
return TYPES['Window'](hass, state.entity_id, state.name)
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)
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)
_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 == 'climate':
support_auto = False
features = state.attributes.get(ATTR_SUPPORTED_FEATURES)
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
if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \
and (features & SUPPORT_TARGET_TEMPERATURE_LOW):
support_auto = True
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat')
support_auto = bool(features & support_temp_range)
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
return TYPES['Thermostat'](hass, state.entity_id,
state.name, support_auto)
state.name, support_auto, aid=aid)
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)
elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean':
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch')
return TYPES['Switch'](hass, state.entity_id, state.name)
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)
return None
def generate_aid(entity_id):
"""Generate accessory aid with zlib adler32."""
aid = adler32(entity_id.encode('utf-8'))
if aid == 0 or aid == 1:
return None
return aid
class HomeKit():
"""Class to handle all actions between HomeKit and Home Assistant."""
def __init__(self, hass, port):
def __init__(self, hass, port, entity_filter, entity_config):
"""Initialize a HomeKit object."""
self._hass = hass
self._port = port
self._filter = entity_filter
self._config = entity_config
self.started = False
self.bridge = None
self.driver = None
def setup_bridge(self, pin):
"""Setup the bridge component to track all accessories."""
from .accessories import HomeBridge
self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin)
def setup(self):
"""Setup bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver
def start_driver(self, event):
"""Start the accessory driver."""
from pyhap.accessory_driver import AccessoryDriver
self._hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop_driver)
self._hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop)
import_types()
_LOGGER.debug("Start adding accessories.")
for state in self._hass.states.all():
acc = get_accessory(self._hass, state)
if acc is not None:
self.bridge.add_accessory(acc)
ip_address = get_local_ip()
path = self._hass.config.path(HOMEKIT_FILE)
self.driver = AccessoryDriver(self.bridge, self._port,
ip_address, path)
_LOGGER.debug("Driver started")
self.bridge = HomeBridge(self._hass)
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand."""
if not state or not self._filter(state.entity_id):
return
aid = generate_aid(state.entity_id)
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self._hass, state, aid, conf)
if acc is not None:
self.bridge.add_accessory(acc)
def start(self, *args):
"""Start the accessory driver."""
if self.started:
return
self.started = True
# pylint: disable=unused-variable
from . import ( # noqa F401
type_covers, type_lights, type_security_systems, type_sensors,
type_switches, type_thermostats)
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)
_LOGGER.debug('Driver start')
self.driver.start()
def stop_driver(self, event):
def stop(self, *args):
"""Stop the accessory driver."""
_LOGGER.debug("Driver stop")
if self.driver is not None:
if not self.started:
return
_LOGGER.debug('Driver stop')
if self.driver and self.driver.run_sentinel:
self.driver.stop()

View File

@@ -2,15 +2,33 @@
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 .const import (
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER)
ACCESSORY_MODEL, ACCESSORY_NAME, 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 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
service = get_serv_loader().get(service)
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
char = get_char_loader().get(char_name)
service.add_characteristic(char)
acc.add_service(service)
return service
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
@@ -21,50 +39,60 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
def add_preload_service(acc, service, chars=None, opt_chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
service = get_serv_loader().get(service)
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
char = get_char_loader().get(char_name)
service.add_characteristic(char)
if opt_chars:
opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars]
for opt_char_name in opt_chars:
opt_char = get_char_loader().get(opt_char_name)
service.add_opt_characteristic(opt_char)
acc.add_service(service)
return service
def override_properties(char, new_properties):
"""Override characteristic property values."""
char.properties.update(new_properties)
class HomeAccessory(Accessory):
"""Class to extend the Accessory class."""
"""Adapter class for Accessory."""
def __init__(self, display_name, model, category='OTHER', **kwargs):
# pylint: disable=no-member
def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
category='OTHER', **kwargs):
"""Initialize a Accessory object."""
super().__init__(display_name, **kwargs)
set_accessory_info(self, display_name, model)
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
self.category = getattr(Category, category, Category.OTHER)
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
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)
async_track_state_change(
self.hass, self.entity_id, self.update_state)
class HomeBridge(Bridge):
"""Class to extend the Bridge class."""
"""Adapter class for Bridge."""
def __init__(self, display_name, model, pincode, **kwargs):
def __init__(self, hass, name=BRIDGE_NAME,
model=BRIDGE_MODEL, **kwargs):
"""Initialize a Bridge object."""
super().__init__(display_name, pincode=pincode, **kwargs)
set_accessory_info(self, display_name, model)
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
add_preload_service(self, SERV_BRIDGING_STATE)
def setup_message(self):
"""Prevent print of pyhap setup message to terminal."""
pass
def add_paired_client(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
super().add_paired_client(client_uuid, client_public)
dismiss_setup_message(self.hass)
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)
class HomeDriver(AccessoryDriver):
"""Adapter class for AccessoryDriver."""
def __init__(self, *args, **kwargs):
"""Initialize a AccessoryDriver object."""
super().__init__(*args, **kwargs)

View File

@@ -1,31 +1,67 @@
"""Constants used be the HomeKit component."""
# #### MISC ####
DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548
# #### CONFIG ####
CONF_AUTO_START = 'auto_start'
CONF_ENTITY_CONFIG = 'entity_config'
CONF_FILTER = 'filter'
# #### CONFIG DEFAULTS ####
DEFAULT_AUTO_START = True
DEFAULT_PORT = 51827
# #### HOMEKIT COMPONENT SERVICES ####
SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS ####
ACCESSORY_MODEL = 'homekit.accessory'
ACCESSORY_NAME = 'Home Accessory'
BRIDGE_MODEL = 'homekit.bridge'
BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant'
# Services
# #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_SENSOR = 'SENSOR'
CATEGORY_SWITCH = 'SWITCH'
CATEGORY_THERMOSTAT = 'THERMOSTAT'
CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_BRIDGING_STATE = 'BridgingState'
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
# StatusLowBattery, Name
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
SERV_SECURITY_SYSTEM = 'SecuritySystem'
SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering'
# Characteristics
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
CHAR_CATEGORY = 'Category'
# #### Characteristics ####
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition'
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model'
CHAR_NAME = 'Name'
CHAR_ON = 'On'
CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState'
CHAR_REACHABLE = 'Reachable'
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
CHAR_TARGET_POSITION = 'TargetPosition'
@@ -33,5 +69,5 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# Properties
# #### Properties ####
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}

View File

@@ -1,84 +0,0 @@
"""Class to hold all cover accessories."""
import logging
from homeassistant.components.cover import ATTR_CURRENT_POSITION
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
_LOGGER = logging.getLogger(__name__)
@TYPES.register('Window')
class Window(HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: set_cover_position.
"""
def __init__(self, hass, entity_id, display_name):
"""Initialize a Window accessory object."""
super().__init__(display_name, entity_id, 'WINDOW')
self._hass = hass
self._entity_id = entity_id
self.current_position = None
self.homekit_target = None
self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = self.serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = self.serv_cover. \
get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = self.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
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_cover_position(new_state=state)
async_track_state_change(
self._hass, self._entity_id, self.update_cover_position)
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
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.services.call(
'cover', 'set_cover_position',
{'entity_id': self._entity_id, 'position': value})
def update_cover_position(self, entity_id=None, old_state=None,
new_state=None):
"""Update cover position after state changed."""
if new_state is None:
return
current_position = new_state.attributes[ATTR_CURRENT_POSITION]
if current_position is None:
return
self.current_position = int(current_position)
self.char_current_position.set_value(self.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)
self.homekit_target = None

View File

@@ -1,72 +0,0 @@
"""Class to hold all sensor accessories."""
import logging
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS)
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import (
HomeAccessory, add_preload_service, override_properties)
from .const import (
SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
_LOGGER = logging.getLogger(__name__)
def calc_temperature(state, unit=TEMP_CELSIUS):
"""Calculate temperature from state and unit.
Always return temperature as Celsius value.
Conversion is handled on the device.
"""
try:
value = float(state)
except ValueError:
return None
return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value
@TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory):
"""Generate a TemperatureSensor accessory for a temperature sensor.
Sensor entity must return temperature in °C, °F.
"""
def __init__(self, hass, entity_id, display_name):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(display_name, entity_id, 'SENSOR')
self._hass = hass
self._entity_id = entity_id
self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = self.serv_temp. \
get_characteristic(CHAR_CURRENT_TEMPERATURE)
override_properties(self.char_temp, PROP_CELSIUS)
self.char_temp.value = 0
self.unit = None
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_temperature(new_state=state)
async_track_state_change(
self._hass, self._entity_id, self.update_temperature)
def update_temperature(self, entity_id=None, old_state=None,
new_state=None):
"""Update temperature after state changed."""
if new_state is None:
return
unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
temperature = calc_temperature(new_state.state, unit)
if temperature is not None:
self.char_temp.set_value(temperature)
_LOGGER.debug("%s: Current temperature set to %d°C",
self._entity_id, temperature)

View File

@@ -0,0 +1,4 @@
# Describes the format for available HomeKit services
start:
description: Starts the HomeKit component driver.

View File

@@ -1,62 +0,0 @@
"""Class to hold all switch accessories."""
import logging
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import split_entity_id
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__)
@TYPES.register('Switch')
class Switch(HomeAccessory):
"""Generate a Switch accessory."""
def __init__(self, hass, entity_id, display_name):
"""Initialize a Switch accessory object to represent a remote."""
super().__init__(display_name, entity_id, 'SWITCH')
self._hass = hass
self._entity_id = entity_id
self._domain = split_entity_id(entity_id)[0]
self.flag_target_state = False
self.service_switch = add_preload_service(self, SERV_SWITCH)
self.char_on = self.service_switch.get_characteristic(CHAR_ON)
self.char_on.value = False
self.char_on.setter_callback = self.set_state
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_state(new_state=state)
async_track_state_change(self._hass, self._entity_id,
self.update_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
service = 'turn_on' if value else '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):
"""Update switch state after state changed."""
if new_state is None:
return
current_state = (new_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)
else:
self.flag_target_state = False

View File

@@ -0,0 +1,73 @@
"""Class to hold all cover accessories."""
import logging
from homeassistant.components.cover import ATTR_CURRENT_POSITION
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
_LOGGER = logging.getLogger(__name__)
@TYPES.register('WindowCovering')
class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: set_cover_position.
"""
def __init__(self, hass, entity_id, display_name, **kwargs):
"""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
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
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)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""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)
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)
self.homekit_target = None

View File

@@ -0,0 +1,187 @@
"""Class to hold all light accessories."""
import logging
from homeassistant.components.light import (
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
_LOGGER = logging.getLogger(__name__)
RGB_COLOR = 'rgb_color'
@TYPES.register('Light')
class Light(HomeAccessory):
"""Generate a Light accessory for a light entity.
Currently supports: state, brightness, color temperature, rgb_color.
"""
def __init__(self, hass, entity_id, name, **kwargs):
"""Initialize a new Light accessory object."""
super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs)
self.hass = hass
self.entity_id = entity_id
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False,
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
self._state = 0
self.chars = []
self._features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
if self._features & SUPPORT_BRIGHTNESS:
self.chars.append(CHAR_BRIGHTNESS)
if self._features & SUPPORT_COLOR_TEMP:
self.chars.append(CHAR_COLOR_TEMPERATURE)
if self._features & SUPPORT_COLOR:
self.chars.append(CHAR_HUE)
self.chars.append(CHAR_SATURATION)
self._hue = None
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
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
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
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
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
def set_state(self, value):
"""Set state if call came from HomeKit."""
if self._state == value:
return
_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)
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)
else:
self.hass.components.light.turn_off(self.entity_id)
def set_color_temperature(self, value):
"""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()
def set_hue(self, value):
"""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()
def set_color(self):
"""Set color if call came from HomeKit."""
# Handle Color
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
self._flag[CHAR_SATURATION]:
color = (self._hue, self._saturation)
_LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color)
self._flag.update({
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
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):
"""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._flag[CHAR_ON] = False
# Handle Brightness
if CHAR_BRIGHTNESS in self.chars:
brightness = new_state.attributes.get(ATTR_BRIGHTNESS)
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._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)
self._flag[CHAR_COLOR_TEMPERATURE] = False
# Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
hue, saturation = new_state.attributes.get(
ATTR_HS_COLOR, (None, None))
if not self._flag[RGB_COLOR] and (
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._hue, self._saturation = (hue, saturation)
self._flag[RGB_COLOR] = False

View File

@@ -5,13 +5,12 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
ATTR_ENTITY_ID, ATTR_CODE)
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
CHAR_TARGET_SECURITY_STATE)
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
_LOGGER = logging.getLogger(__name__)
@@ -28,49 +27,42 @@ 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=None):
def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
"""Initialize a SecuritySystem accessory object."""
super().__init__(display_name, entity_id, 'ALARM_SYSTEM')
super().__init__(display_name, entity_id,
CATEGORY_ALARM_SYSTEM, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
self._alarm_code = alarm_code
self.flag_target_state = False
self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
self.char_current_state = self.service_alarm. \
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 = self.service_alarm. \
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
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_security_state(new_state=state)
async_track_state_change(self._hass, self._entity_id,
self.update_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)
_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]
params = {ATTR_ENTITY_ID: self._entity_id}
if self._alarm_code is not None:
params = {ATTR_ENTITY_ID: self.entity_id}
if self._alarm_code:
params[ATTR_CODE] = self._alarm_code
self._hass.services.call('alarm_control_panel', service, params)
self.hass.services.call('alarm_control_panel', service, params)
def update_security_state(self, entity_id=None,
old_state=None, new_state=None):
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update security state after state changed."""
if new_state is None:
return
@@ -78,15 +70,15 @@ class SecuritySystem(HomeAccessory):
hass_state = new_state.state
if hass_state not in HASS_TO_HOMEKIT:
return
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)
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)
elif self.char_target_state.get_value() \
== self.char_current_state.get_value():
if self.char_target_state.value == self.char_current_state.value:
self.flag_target_state = False

View File

@@ -0,0 +1,77 @@
"""Class to hold all sensor accessories."""
import logging
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
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
_LOGGER = logging.getLogger(__name__)
@TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory):
"""Generate a TemperatureSensor accessory for a temperature sensor.
Sensor entity must return temperature in °C, °F.
"""
def __init__(self, hass, entity_id, name, **kwargs):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
self.hass = hass
self.entity_id = entity_id
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.unit = None
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""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)
_LOGGER.debug('%s: Current temperature set to %d°C',
self.entity_id, temperature)
@TYPES.register('HumiditySensor')
class HumiditySensor(HomeAccessory):
"""Generate a HumiditySensor accessory as humidity sensor."""
def __init__(self, hass, entity_id, name, *args, **kwargs):
"""Initialize a HumiditySensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
self.hass = hass
self.entity_id = entity_id
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity \
.get_characteristic(CHAR_CURRENT_HUMIDITY)
self.char_humidity.value = 0
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""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)
_LOGGER.debug('%s: Percent set to %d%%',
self.entity_id, humidity)

View File

@@ -0,0 +1,55 @@
"""Class to hold all switch accessories."""
import logging
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
from homeassistant.core import split_entity_id
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__)
@TYPES.register('Switch')
class Switch(HomeAccessory):
"""Generate a Switch accessory."""
def __init__(self, hass, entity_id, display_name, **kwargs):
"""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]
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
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):
"""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.flag_target_state = False

View File

@@ -7,21 +7,19 @@ from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.helpers.event import async_track_state_change
ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__)
STATE_OFF = 'off'
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
@@ -33,61 +31,62 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate."""
def __init__(self, hass, entity_id, display_name, support_auto=False):
def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
"""Initialize a Thermostat accessory object."""
super().__init__(display_name, entity_id, 'THERMOSTAT')
super().__init__(display_name, entity_id,
CATEGORY_THERMOSTAT, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
self._call_timer = None
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
extra_chars = None
# Add additional characteristics if auto mode is supported
if support_auto:
extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE]
extra_chars = [
CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None
# Preload the thermostat service
self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT,
extra_chars)
serv_thermostat = add_preload_service(self, SERV_THERMOSTAT,
extra_chars)
# Current and target mode characteristics
self.char_current_heat_cool = self.service_thermostat. \
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 = self.service_thermostat. \
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
# Current and target temperature characteristics
self.char_current_temp = self.service_thermostat. \
self.char_current_temp = serv_thermostat. \
get_characteristic(CHAR_CURRENT_TEMPERATURE)
self.char_current_temp.value = 21.0
self.char_target_temp = self.service_thermostat. \
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
# Display units characteristic
self.char_display_units = self.service_thermostat. \
self.char_display_units = serv_thermostat. \
get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
self.char_display_units.value = 0
# If the device supports it: high and low temperature characteristics
if support_auto:
self.char_cooling_thresh_temp = self.service_thermostat. \
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 = self.service_thermostat. \
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 = \
@@ -96,132 +95,127 @@ class Thermostat(HomeAccessory):
self.char_cooling_thresh_temp = None
self.char_heating_thresh_temp = None
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_thermostat(new_state=state)
async_track_state_change(self._hass, self._entity_id,
self.update_thermostat)
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)
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.heat_cool_flag_target_state = True
hass_value = HC_HOMEKIT_TO_HASS[value]
self._hass.services.call('climate', 'set_operation_mode',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_OPERATION_MODE: hass_value})
self.hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self.entity_id)
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",
self._entity_id, value)
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
self.entity_id, value)
self.coolingthresh_flag_target_state = True
low = self.char_heating_thresh_temp.get_value()
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TARGET_TEMP_HIGH: value,
ATTR_TARGET_TEMP_LOW: low})
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)
self.hass.components.climate.set_temperature(
entity_id=self.entity_id, target_temp_high=value,
target_temp_low=low)
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",
self._entity_id, value)
_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.get_value()
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TARGET_TEMP_LOW: value,
ATTR_TARGET_TEMP_HIGH: high})
high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit)
value = temperature_to_states(value, self._unit)
self.hass.components.climate.set_temperature(
entity_id=self.entity_id, target_temp_high=high,
target_temp_low=value)
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target temperature to %.2f",
self._entity_id, value)
_LOGGER.debug('%s: Set target temperature to %.2f°C',
self.entity_id, value)
self.temperature_flag_target_state = True
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TEMPERATURE: value})
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_thermostat(self, entity_id=None,
old_state=None, new_state=None):
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update security state after state changed."""
if new_state is None:
return
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS)
# Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if current_temp is not None:
if isinstance(current_temp, (int, float)):
current_temp = temperature_to_homekit(current_temp, self._unit)
self.char_current_temp.set_value(current_temp)
# Update target temperature
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
if target_temp is not None:
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)
else:
self.temperature_flag_target_state = False
self.temperature_flag_target_state = False
# Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp is not None:
if self.char_cooling_thresh_temp:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
if cooling_thresh is not None:
if isinstance(cooling_thresh, (int, float)):
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)
else:
self.coolingthresh_flag_target_state = False
self.coolingthresh_flag_target_state = False
# Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp is not None:
if self.char_heating_thresh_temp:
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
if heating_thresh is not None:
if isinstance(heating_thresh, (int, float)):
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)
else:
self.heatingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False
# Update display units
display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if display_units is not None \
and display_units in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(
UNIT_HASS_TO_HOMEKIT[display_units])
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit])
# Update target operation mode
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
if operation_mode is not None \
if operation_mode \
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)
else:
self.heat_cool_flag_target_state = False
self.heat_cool_flag_target_state = False
# Set current operation mode based on temperatures and target mode
if operation_mode == STATE_HEAT:
if current_temp < target_temp:
if isinstance(target_temp, float) and current_temp < target_temp:
current_operation_mode = STATE_HEAT
else:
current_operation_mode = STATE_OFF
elif operation_mode == STATE_COOL:
if current_temp > target_temp:
if isinstance(target_temp, float) and current_temp > target_temp:
current_operation_mode = STATE_COOL
else:
current_operation_mode = STATE_OFF
elif operation_mode == STATE_AUTO:
# Check if auto is supported
if self.char_cooling_thresh_temp is not None:
lower_temp = self.char_heating_thresh_temp.get_value()
upper_temp = self.char_cooling_thresh_temp.get_value()
if self.char_cooling_thresh_temp:
lower_temp = self.char_heating_thresh_temp.value
upper_temp = self.char_cooling_thresh_temp.value
if current_temp < lower_temp:
current_operation_mode = STATE_HEAT
elif current_temp > upper_temp:
@@ -232,9 +226,11 @@ class Thermostat(HomeAccessory):
# Check if heating or cooling are supported
heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST]
cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST]
if current_temp < target_temp and heat:
if isinstance(target_temp, float) and \
current_temp < target_temp and heat:
current_operation_mode = STATE_HEAT
elif current_temp > target_temp and cool:
elif isinstance(target_temp, float) and \
current_temp > target_temp and cool:
current_operation_mode = STATE_COOL
else:
current_operation_mode = STATE_OFF

View File

@@ -0,0 +1,66 @@
"""Collection of useful functions for the HomeKit component."""
import logging
import voluptuous as vol
from homeassistant.core import split_entity_id
from homeassistant.const import (
ATTR_CODE, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.temperature as temp_util
from .const import HOMEKIT_NOTIFY_ID
_LOGGER = logging.getLogger(__name__)
def validate_entity_config(values):
"""Validate config entry for CONF_ENTITY."""
entities = {}
for key, config in values.items():
entity = cv.entity_id(key)
params = {}
if not isinstance(config, dict):
raise vol.Invalid('The configuration for "{}" must be '
' an dictionary.'.format(entity))
domain, _ = split_entity_id(entity)
if domain == 'alarm_control_panel':
code = config.get(ATTR_CODE)
params[ATTR_CODE] = cv.string(code) if code else None
entities[entity] = params
return entities
def show_setup_message(bridge, hass):
"""Display persistent notification with setup information."""
pin = bridge.pincode.decode()
_LOGGER.info('Pincode: %s', pin)
message = 'To setup Home Assistant in the Home App, enter the ' \
'following code:\n### {}'.format(pin)
hass.components.persistent_notification.create(
message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID)
def dismiss_setup_message(hass):
"""Dismiss persistent notification and remove QR code."""
hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID)
def convert_to_float(state):
"""Return float of state, catch errors."""
try:
return float(state)
except (ValueError, TypeError):
return None
def temperature_to_homekit(temperature, unit):
"""Convert temperature to Celsius for HomeKit."""
return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)
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)

View File

@@ -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.39']
REQUIREMENTS = ['pyhomematic==0.1.40']
DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__)
@@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor'
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
DISCOVER_COVER = 'homematic.cover'
DISCOVER_CLIMATE = 'homematic.climate'
DISCOVER_LOCKS = 'homematic.locks'
ATTR_DISCOVER_DEVICES = 'devices'
ATTR_PARAM = 'param'
@@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode'
HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren',
'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'],
DISCOVER_SENSORS: [
'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP',
@@ -68,7 +69,7 @@ HM_DEVICE_TYPES = {
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke', 'RFSiren', 'PresenceIP'],
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'],
DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@@ -78,7 +79,8 @@ HM_DEVICE_TYPES = {
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
'WiredSensor', 'PresenceIP'],
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt']
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
DISCOVER_LOCKS: ['KeyMatic']
}
HM_IGNORE_DISCOVERY_NODE = [
@@ -86,6 +88,10 @@ HM_IGNORE_DISCOVERY_NODE = [
'ACTUAL_HUMIDITY'
]
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
'ACTUAL_TEMPERATURE': ['IPAreaThermostat'],
}
HM_ATTRIBUTE_SUPPORT = {
'LOWBAT': ['battery', {0: 'High', 1: 'Low'}],
'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}],
@@ -460,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args):
('cover', DISCOVER_COVER),
('binary_sensor', DISCOVER_BINARY_SENSORS),
('sensor', DISCOVER_SENSORS),
('climate', DISCOVER_CLIMATE)):
('climate', DISCOVER_CLIMATE),
('lock', DISCOVER_LOCKS)):
# Get all devices of a specific type
found_devices = _get_devices(
hass, discovery_type, addresses, interface)
@@ -505,7 +512,8 @@ def _get_devices(hass, discovery_type, keys, interface):
# Generate options for 1...n elements with 1...n parameters
for param, channels in metadata.items():
if param in HM_IGNORE_DISCOVERY_NODE:
if param in HM_IGNORE_DISCOVERY_NODE and class_name not in \
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []):
continue
# Add devices

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