Compare commits

...

332 Commits

Author SHA1 Message Date
Paulus Schoutsen
6603b3eccd Merge pull request #19228 from home-assistant/rc
0.84.1
2018-12-12 17:54:37 +01:00
Paulus Schoutsen
e2bf3ac095 Bumped version to 0.84.1 2018-12-12 17:18:47 +01:00
Paulus Schoutsen
ced96775fe Fix owntracks topic in encrypted ios (#19220)
* Fix owntracks topic

* Warn if per-topic secret and using HTTP
2018-12-12 17:18:38 +01:00
Jason Hunter
f65e57bf7b Add automation and script events to logbook event types (#19219) 2018-12-12 17:18:37 +01:00
Paulus Schoutsen
88cda043ac Merge pull request #19215 from home-assistant/rc
0.84
2018-12-12 14:17:53 +01:00
Paulus Schoutsen
404fbe388c Bumped version to 0.84.0 2018-12-12 11:45:42 +01:00
Paulus Schoutsen
a0bc96c20d Revert PR #18602 (#19188) 2018-12-12 11:45:20 +01:00
Paulus Schoutsen
e98476e026 Bumped version to 0.84.0b4 2018-12-11 10:33:58 +01:00
Paulus Schoutsen
aa45ff83bd Fix cloud defaults (#19172) 2018-12-11 10:33:21 +01:00
Paulus Schoutsen
029d006beb Updated frontend to 20181211.0 2018-12-11 10:30:35 +01:00
Paulus Schoutsen
e94eb686a6 Bumped version to 0.84.0b3 2018-12-10 13:00:41 +01:00
Paulus Schoutsen
2da5a02285 Add raw service data to event (#19163) 2018-12-10 13:00:35 +01:00
Paulus Schoutsen
e3b1008511 Fix lovelace save (#19162) 2018-12-10 13:00:34 +01:00
Paulus Schoutsen
cb874fefbb Drop OwnTracks bad packets (#19161) 2018-12-10 13:00:34 +01:00
phnx
0454a5fa3f home-assistant/home-assistant#18645: revert heat-cool -> auto change 2018-12-10 13:00:09 +01:00
phnx
d8f6331318 home-assistant/home-assistant#18645: Remove un-used constants. 2018-12-10 13:00:09 +01:00
phnx
d7459c73e0 home-assistant/home-assistant#18645: Fix climate mode mapping. 2018-12-10 13:00:09 +01:00
Eric Nagley
fa9fe4067a Google assistant fix target temp for *F values. (#19083)
* home-assistant/home-assistant#18524 : Add rounding to *F temps

* home-assistant/home-assistant#18524 : Linting

* simplify round behavior

* fix trailing whitespace

(thanks github editor)
2018-12-10 12:59:16 +01:00
Paulus Schoutsen
55aaa894c3 Updated frontend to 20181210.1 2018-12-10 12:50:42 +01:00
Paulus Schoutsen
18bc772cbb Bumped version to 0.84.0b2 2018-12-10 09:55:19 +01:00
arigilder
a5072f0fe4 Remove marking device tracker stale if state is stale (#19133) 2018-12-10 09:54:37 +01:00
Paulus Schoutsen
76c26da4cb Lovelace using storage (#19101)
* Add MVP

* Remove unused code

* Fix

* Add force back

* Fix tests

* Storage keyed

* Error out when storage doesnt find config

* Use old load_yaml

* Set config for panel correct

* Use instance cache var

* Make config option
2018-12-10 09:54:36 +01:00
Nick Horvath
3528d865b7 Bump skybellpy version to fix api issue (#19100) 2018-12-10 09:54:36 +01:00
Daniel Høyer Iversen
e6c224fa40 Upgrade Tibber lib (#19098) 2018-12-10 09:54:35 +01:00
Pierre Ståhl
048f219a7f Upgrade pyatv to 0.3.12 (#19085) 2018-12-10 09:54:35 +01:00
Paulus Schoutsen
945b84a7df Updated frontend to 20181210.0 2018-12-10 09:54:20 +01:00
Paulus Schoutsen
393ada0312 Bumped version to 0.84.0b1 2018-12-07 07:14:19 +01:00
Anders Melchiorsen
da160066c3 Upgrade aiolifx to 0.6.7 (#19077) 2018-12-07 07:13:35 +01:00
Bram Kragten
ff9427d463 Force refresh Lovelace (#19073)
* Force refresh Lovelace

* Check config on load

* Update __init__.py

* Update __init__.py
2018-12-07 07:13:35 +01:00
Mike Miller
3eb646eb0d Fix missing colorTemperatureInKelvin from Alexa responses (#19069)
* Fix missing colorTemperatureInKelvin from Alexa responses

* Update smart_home.py

* Add test
2018-12-07 07:13:34 +01:00
Paulus Schoutsen
578fe371c6 Revert #17745 (#19064) 2018-12-07 07:13:34 +01:00
Paulus Schoutsen
1b03a35fa1 Updated frontend to 20181207.0 2018-12-07 07:13:23 +01:00
Paulus Schoutsen
4fd4e84b72 Bumped version to 0.84.0b0 2018-12-06 09:34:21 +01:00
Daniel Høyer Iversen
d4c8024522 Add support for more Tibber Pulse data (#19033) 2018-12-06 09:30:11 +01:00
Martin Gross
72379c166e Update locationsharinglib to 3.0.9 (#19045) 2018-12-06 09:29:30 +01:00
Paulus Schoutsen
f198706767 Remove Instapush notify platform (#19051) 2018-12-06 09:28:31 +01:00
pbalogh77
f0d534cebc Implemented unique ID support for Fibaro hub integration (#19055)
* Unique ID support

New unique ID support, based on hub's serial number and device's permanent ID

* Fixes, showing attributes

Minor fixes
Showing room, hub, fibaro_id for easier mapping and finding of devices

* Update fibaro.py
2018-12-06 09:28:06 +01:00
Daniel Perna
47320adcc6 Update pyhomematic to 0.1.53 (#19056) 2018-12-06 09:25:39 +01:00
Bram Kragten
b9ed4b7a76 Fix saving YAML as JSON with empty array (#19057)
* Fix saving YAML as JSON with empty array

* Lint
2018-12-06 09:24:49 +01:00
Erik Eriksson
b71d65015a VOC: Update external dependency to fix engine start issue (#19062) 2018-12-06 09:22:49 +01:00
Paulus Schoutsen
962358bf87 Fix cloud const (#19052)
* Fix cloud const

* Fix tests
2018-12-06 09:20:53 +01:00
Paulus Schoutsen
26a38f1fae Updated frontend to 20181205.0 2018-12-06 00:30:45 +01:00
Paulus Schoutsen
83311df933 Add translations 2018-12-06 00:30:33 +01:00
emontnemery
06285d1bf3 Merge pull request #19019 from emontnemery/fix_mqtt_availability
Fix entity unavailable after reconfiguring MQTT availability
2018-12-05 22:14:37 +01:00
photinus
0aee355b14 Bump pyvizio version (#19048)
* Update vizio.py

Bump pyvizio version to reoslve get volume call and component setup failures

* Update of requirement_all
2018-12-05 16:00:49 -05:00
Fabian Affolter
b2b4712bb7 Remove Instapush notify platform 2018-12-05 21:17:02 +01:00
Sebastian Muszynski
af96694430 Remove unsupported strong mode of the Xiaomi Air Humidifier CA1 (#18926)
* Remove unsupported strong mode of the Xiaomi Air Humidifier CA1

* Clean up filter of unsupported modes
2018-12-05 20:56:43 +01:00
Erik
df346feb65 Review comments 2018-12-05 19:48:44 +01:00
Aaron Bach
08702548f3 Add support for multiple RainMachine controllers (#18989)
* Add support for multiple RainMachine controllers

* Member comments

* Member comments

* Member comments

* Cleanup

* More config flow cleanup

* Member comments
2018-12-05 10:31:32 -07:00
Teemu R
bc69309b46 Add last clean times to xiaomi vacuum (#19043) 2018-12-05 18:20:26 +01:00
Teemu R
da0542e961 Bump python-miio to 0.4.4 (#19042) 2018-12-05 18:19:30 +01:00
Jeff Irion
16e25f2039 Catch 'BrokenPipeError' exceptions for ADB commands (#19011) 2018-12-05 17:04:08 +01:00
Sébastien RAMAGE
3627de3e8a Change error to warning (#19035) 2018-12-05 15:58:46 +01:00
Glenn Waters
b31c52419d Bump version of elkm1_lib (#19030) 2018-12-05 09:31:07 -05:00
Paulus Schoutsen
578a2cf357 Small refactoring of MQTT switch (#19010) 2018-12-05 14:47:02 +01:00
emontnemery
69fd3aa856 Small refactoring of MQTT light (#19009) 2018-12-05 14:46:37 +01:00
Paulus Schoutsen
12f222b5e3 Don't wait for answer for webhook register (#19025) 2018-12-05 14:45:30 +01:00
Paulus Schoutsen
eb317bd302 Merge pull request #19036 from sdague/mychevy_1.0.1
update mychevy to 1.0.1
2018-12-05 14:44:43 +01:00
Paulus Schoutsen
ab9d1a83af Bump waterfurnace to 1.0 (#19040)
This bumps to the new version of the waterfurnace API. In the new
version the unit id is no longer manually set by the user, instead it
is retrieved from the service after login. This is less error prone as
it turns out discovering the correct unit id is hard from an end user
perspective.

Breaking change on the config, as the unit parameter is removed from
config. However I believe the number of users is very low (possibly
only 2), so adaptation should be easy.
2018-12-05 14:44:25 +01:00
Paulus Schoutsen
0e9e253b7b Fix CI by pinning IDNA (#19038)
* Fix CI

* Actual fix by @sdague
2018-12-05 14:43:29 +01:00
Bram Kragten
850caef5c1 Add states to panels (#19026)
* Add states to panels

* Line too long

* remove extra urls for states

* Update __init__.py
2018-12-05 14:27:35 +01:00
Sean Dague
8c0b50b5df Bump waterfurnace to 1.0
This bumps to the new version of the waterfurnace API. In the new
version the unit id is no longer manually set by the user, instead it
is retrieved from the service after login. This is less error prone as
it turns out discovering the correct unit id is hard from an end user
perspective.

Breaking change on the config, as the unit parameter is removed from
config. However I believe the number of users is very low (possibly
only 2), so adaptation should be easy.
2018-12-05 07:03:27 -05:00
Sean Dague
a785a1ab5d update mychevy to 1.0.1
After six months the chevy website finally has been reimplemented to
something that seems to work and is stable. The backend library has
been updated thanks to upstream help, and now is working again.
2018-12-05 05:42:27 -05:00
Paulus Schoutsen
3928d034a3 Allow checking entity permissions based on devices (#19007)
* Allow checking entity permissions based on devices

* Fix tests
2018-12-05 11:41:00 +01:00
jxwolstenholme
2680bf8a61 Update requirement btsmarthub_devicelist==0.1.3 (#18961)
* Added requirement 'btsmarthub_devicelist==0.1.2'

* Update requirements_all.txt

* Update bt_smarthub.py

* Update requirements_all.txt

* Update bt_smarthub.py
2018-12-04 23:26:20 +01:00
majuss
a8b5cc833d Lupupy version push to 0.0.17 - will now transmitted state_alarm_triggered (#19008)
* added state_alarm_triggered transmission; pushed lupupy version

* added state_alarm_triggered transmission; pushed lupupy version

* added state_alarm_triggered transmission; pushed lupupy version

* added state_alarm_triggered transmission; pushed lupupy version
2018-12-04 22:04:39 +01:00
Erik
f54710c454 Fix bug when reconfiguring MQTT availability 2018-12-04 21:25:18 +01:00
Erik
47d48c5990 Small refactoring of MQTT switch 2018-12-04 16:39:49 +01:00
Matt Hamilton
38b09b1613 Remove stale user salts code (#19004)
user['salt'] was originally used as a part of the pbkdf2 implementation.
I failed to remove this as a part of the cleanup in #18736.
2018-12-04 14:39:43 +01:00
Paulus Schoutsen
26dd490e8e Fix toon operation mode (#18966)
* Fix toon

* Update toon.py
2018-12-04 13:24:18 +01:00
Emil Stjerneman
1c99960357 Fix VOC configuration resource list (#18992) 2018-12-04 11:39:42 +01:00
Alexei Chetroi
3e1ab1b23a Sort import order of zha component. (#18993) 2018-12-04 11:38:57 +01:00
pbalogh77
2a0c2d5247 Fibaro Light fixes (#18972)
* minor fixes to color scaling

* capped input to fibaro on setcolor
2018-12-04 11:38:21 +01:00
Paulus Schoutsen
b65bffd849 Mock out device tracker configuration loading funcs in Geofency + OwnTracks (#18968)
* Mock out device tracker configuration loading funcs

* Update test_init.py

* Update test_init.py
2018-12-04 10:45:41 +01:00
cdce8p
ab7c52a9c4 Add unnecessary-pass for pylint-update (#18985) 2018-12-04 10:45:16 +01:00
Fredrik Erlandsson
d6a4e106a9 Tellduslive refactoring (#18780)
* move component to a package

* move TelldusLiveEntry to separate file

* refactor

* move entities from a shared container
* using the dispatch helper instead for communication between component and platforms

* updated covereagerc and codeowners

* suggestions from MartinHjelmare

* don't make update async

* "Strip is good!"
2018-12-04 10:08:40 +01:00
Pierre Gronlier
a6511fc0b9 remove the need to have query feature support (#18942)
* remove the need to have query feature support

Some InfluxDB servers don't have /query support feature but are still valid servers for storing data.
Usually those servers are proxies to others timeseries databases.
The change proposes to still validate the configuration but with less requirements on the server side.

* `.query` call is replaced by `.write_points`

* no more query call in the influxdb component. remove test

* reset mock after the setup and before the test

* remove unused import

* reset mock stats after component setup
2018-12-04 09:59:03 +01:00
Bram Kragten
75b855ef93 Lovelace fix: badges are removed from view after update (#18983)
* badges are removed from view after update

* Only add badges and cards when not provided in new config
2018-12-04 09:56:30 +01:00
Dom
d8a7e9ded8 Updated Yale Smart Alarm platform to new Yale API (#18990)
* Updated Yale Smart Alarm platform to use Yale's new API which replaces the deprecated version. Bumped yalesmartalarmclient to v0.1.5.

* Update requirements
2018-12-04 09:56:14 +01:00
Craig J. Midwinter
f3d7cc66e5 downgrade version of client (#18995)
* downgrade version of client

* update requirements
2018-12-04 09:52:30 +01:00
Jason Hunter
b900005d1e New Events and Context Fixes (#18765)
* Add new events for automation trigger and script run, fix context for image processing, add tests to ensure same context

* remove custom logbook entry for automation and add new automation event to logbook

* code review updates
2018-12-04 09:45:17 +01:00
Daniel Høyer Iversen
8e9c73eb18 Upgrade switchbot lib (#18980) 2018-12-04 06:48:27 +01:00
Daniel Høyer Iversen
b024c3a833 Add @danielhiversen as codeowner (#18979) 2018-12-04 06:48:16 +01:00
Daniel Høyer Iversen
31078b2b3e Merge pull request #18928 from home-assistant/tibber_err_handle
Tibber, Improve logging and error handling
2018-12-04 06:47:50 +01:00
Joakim Sørensen
ad0e3cea8a Update CODEOWNERS (#18976) 2018-12-03 21:49:15 -05:00
Bram Kragten
b5e7e45f6c no ordered dict (#18982) 2018-12-03 21:54:34 +01:00
Kevin Fronczak
4486de743d Support for mulitple Blink sync modules (#18663) 2018-12-03 20:45:12 +00:00
Daniel Høyer Iversen
df3c683023 Improve err handling 2018-12-03 20:53:18 +01:00
Erik Eriksson
d7a10136df VOC: Update library version. Moved method one step out. Instruments can be a set as well (#18967) 2018-12-03 19:52:50 +01:00
Otto Winter
c8d92ce907 Fix MQTT re-subscription logic (#18953)
* Fix MQTT re-subscription logic

* Cleanup

* Lint

* Fix
2018-12-03 19:16:58 +01:00
Fredrik Erlandsson
111a3254fb Point fix for multiple devices (#18959)
* fix for multiple devices closes, #18956

* Point API finally supports "all" events
2018-12-03 16:50:05 +01:00
Paulus Schoutsen
d028236bf2 Refactor script helper actions into their own methods (#18962)
* Refactor script helper actions into their own methods

* Lint

* Lint
2018-12-03 15:46:25 +01:00
Bram Kragten
d0751ffd91 Add id when not exist and fix dup id check (#18960)
* Add id when not exist and fix dup id check

* config possibly not be a yaml dict
2018-12-03 15:44:04 +01:00
Paulus Schoutsen
2fff0324f8 Merge remote-tracking branch 'origin/master' into dev 2018-12-03 15:26:55 +01:00
pbalogh77
149eddaf46 Initial scene support for Fibaro hubs (#18779)
* Initial scene support

Added initial support for fibaro scenes

* removed comments

* cleanup based on code review

* Removed unused functions

* grrr, my mistake.

My local pylint and flake8 are playing tricks with me

* Update homeassistant/components/scene/fibaro.py

* fixes based on code review

ABC ordered the list of platforms
changed setup platform to async
removed overloaded name property as the FibaroDevice parent class already provides this
Changed to new style string formatting

* Update homeassistant/components/scene/fibaro.py

Co-Authored-By: pbalogh77 <peter.balogh2@gmail.com>
2018-12-03 14:57:55 +01:00
Paulus Schoutsen
acd2f55d4f Merge pull request #18958 from home-assistant/rc
0.83.3
2018-12-03 13:56:35 +01:00
Paulus Schoutsen
4ef1bf2157 Bumped version to 0.83.3 2018-12-03 11:42:49 +01:00
kennedyshead
106cb63922 bump aioasuswrt version (#18955) 2018-12-03 11:42:39 +01:00
kennedyshead
f6a79059e5 fix aioasuswrt sometimes return empty lists (#18742)
* aioasuswrt sometimes return empty lists

* Bumping aioasuswrt to 1.1.12
2018-12-03 11:42:38 +01:00
Paulus Schoutsen
35690d5b29 Add users added via credentials to admin group too (#18922)
* Add users added via credentials to admin group too

* Update test_init.py
2018-12-03 11:41:04 +01:00
William Scanlon
3575c34f77 Use capability of sensor if present to fix multisensor Wink devices (#18907) 2018-12-03 11:41:03 +01:00
ludeeus
ee1c29b392 corrects , -> . typo 2018-12-03 11:40:50 +01:00
ludeeus
82d89edb4f Use dict.get('key') instead of dict['key'] 2018-12-03 11:40:50 +01:00
ludeeus
475be636d6 Fix requirements_all 2018-12-03 11:40:50 +01:00
ludeeus
f8218b5e01 Fix IndexError for home stats 2018-12-03 11:40:50 +01:00
ludeeus
79a9c1af9e bump ghlocalapi to use clear_scan_result 2018-12-03 11:40:03 +01:00
ludeeus
6de0ed3f0a Fix stability issues with multiple units 2018-12-03 11:40:03 +01:00
Andrew Hayworth
1d717b768d bugfix: ensure the google_assistant component respects allow_unlock (#18874)
The `Config` object specific to the `google_assistant` component
had a default value for `allow_unlock`. We were not overriding this
default when constructing the Config object during `google_assistant`
component setup, whereas we do when setting up the `cloud` component.

To fix, we thread the `allow_unlock` parameter down through http setup,
and ensure that it's set correctly. Moreover, we also change the
ordering of the `Config` parameters, and remove the default. Future
refactoring should not miss it, as it is now a required parameter.
2018-12-03 11:38:43 +01:00
William Scanlon
85c0de550c Use capability of sensor if present to fix multisensor Wink devices (#18907) 2018-12-03 11:34:22 +01:00
Paulus Schoutsen
d2b62840f2 Add users added via credentials to admin group too (#18922)
* Add users added via credentials to admin group too

* Update test_init.py
2018-12-03 11:34:01 +01:00
kennedyshead
17c6ef5d54 bump aioasuswrt version (#18955) 2018-12-03 11:13:06 +01:00
cdce8p
3904d83c32 Extend partial reload to include packages (#18884)
* Merge packages after partial reload

* Remove merge from core reload & test

* Integrate merge in 'async_hass_config_yaml'

* Merge executors
2018-12-03 10:56:26 +01:00
Oliver
f3946cb54f Push to version 0.7.7 of denonavr (#18917) 2018-12-03 10:07:43 +01:00
James Hilliard
832fa61477 Initial hlk-sw16 relay switch support (#17855)
* Initial hlk-sw16 relay switch support

* remove entity_id and validate relay id's

* Bump hlk-sw16 library version and cleanup component

* refactor hlk-sw16 switch platform loading

* Use voluptuous to coerce relay id to string

* remove force_update for SW16Switch

* Move to callback based hlk-sw16 relay state changes

* fix hlk-sw16 default port and cleanup some unused variables

* Refactor to allow registration of multiple HLK-SW16 device

* Store protocol in instance variable instead of class variable

* remove is_connected

* flake8 style fix

* Move reconnect logic into HLK-SW16 client library

* Cleanup and improve logging

* Load hlk-sw16 platform entities at same time per device

* scope SIGNAL_AVAILABILITY to device_id

* Fixes for connection resume

* move device_client out of switches loop

* Add timeout for commands and keep alive

* remove unused variables
2018-12-03 09:31:53 +01:00
Andrew Hayworth
5ae65142b8 Allow verisure locks to be configured with a default code (#18873)
* Allow verisure locks to be configured with a default code

* linting fix

* PR feedback

* PR feedback - try harder to prevent future typos

A python mock is a magical thing, and will respond to basicaly
any method you call on it. It's somewhat better to assert against
an explicit variable named 'mock', rather than to assert on the
method name you wanted to mock... could prevent a typo from messing up
tests.

* PR feedback: convert tests to integration-style tests

Set up a fake verisure hub, stub out a _lot_ of calls, then test
after platform discovery and service calls.

It should be noted that we're overriding the `update()` calls in
these tests. This was done to prevent even further mocking of
the verisure hub's responses.

Hopefully, this'll be a foundation for people to write more tests.

* more pr feedback
2018-12-03 07:25:54 +01:00
GeoffAtHome
eb584a26e2 Add lightwave components for switches and lights (#18026)
* Added lightwave components for switches and lights.

* Address warnings raised by Hound

* Correcting lint messages and major typo. This time tested before commit.

* Trying to fix author

* Minor lint changes

* Attempt to correct other lint error.

* Another lint attempt.

* More lint issues.

* Last two lint errors! Hurrah.

* Changes after review from fabaff.

* Moved device dependent code to PyPi.

* Replaced DEPENDENCIES with REQUIREMENTS

* Updated following code review from Martin Hjelmare.

* Added lightwave to requirements_all.txt

* Omit lightwave from tests.

* Updated requirements_all.txt

* Refactored how lightwave lights and switches load.

* Removed imports that were no longer required.

* Add guard for no discovery_info.

* Make it a guard clause and save indentation. Rename LRFxxx to LWRFxxx.

* Sorted imports to match style guidelines.

* Correct return value.

* Update requirements_all.txt

* Catch case where we have no lights or switches configured.

* Improve configuration validation.
2018-12-02 20:58:31 +01:00
emontnemery
87fb492b14 Remove commented out code (#18925) 2018-12-02 19:12:03 +01:00
Paulus Schoutsen
d1a621601d No more opt-out auth (#18854)
* No more opt-out auth

* Fix var
2018-12-02 16:32:53 +01:00
emontnemery
ae9e3d83d7 Reconfigure MQTT switch component if discovery info is changed (#18179) 2018-12-02 16:16:46 +01:00
emontnemery
afa99915e3 Reconfigure MQTT light component if discovery info is changed (#18176) 2018-12-02 16:16:36 +01:00
Martin Fuchs
bb13829e13 Set sensor to unavailable if battery is dead. (#18802) 2018-12-02 16:01:18 +01:00
Daniel Høyer Iversen
fb12294bb7 remove unused import 2018-12-02 15:54:52 +01:00
Vladimir Eremin
debae6ad2e Fix hdmi_cec entity race (#18753)
* Update shouldn't be called before adding the entity.
* Transitional states from
  8adc786bac/include/cectypes.h (L458-L459)

Addressing https://github.com/home-assistant/home-assistant/issues/12846
2018-12-02 15:51:04 +01:00
Otto Winter
eec4564c71 Show ANSI color codes in logs in Hass.io (#18834)
* Hass.io: Show ANSI color codes in logs

* Lint

* Fix test

* Lint
2018-12-02 15:46:14 +01:00
Daniel Høyer Iversen
08dbd792cd Improve logging and error handling 2018-12-02 15:35:59 +01:00
Andrew Hayworth
b7e2522083 bugfix: ensure the google_assistant component respects allow_unlock (#18874)
The `Config` object specific to the `google_assistant` component
had a default value for `allow_unlock`. We were not overriding this
default when constructing the Config object during `google_assistant`
component setup, whereas we do when setting up the `cloud` component.

To fix, we thread the `allow_unlock` parameter down through http setup,
and ensure that it's set correctly. Moreover, we also change the
ordering of the `Config` parameters, and remove the default. Future
refactoring should not miss it, as it is now a required parameter.
2018-12-02 11:14:46 +01:00
Paulus Schoutsen
a62fc7ca04 Use string formatting (#18886)
* Use string formatting

* Fix lint issue

* Fix change
2018-12-02 10:53:09 +01:00
pbalogh77
0a68cae507 Fibaro ubs (#18889)
* Fibaro HC connection, initial commit

Very first steps working, connects, fetches devices, represents sensors, binary_sensors and lights towards HA.

* Cover, switch, bugfixes

Initial support for covers
Initial support for switches
Bugfixes

* Some cleanup and improved lights

pylint based cleanup
light switches handled properly
light features reported correctly

* Added status updates and actions

Lights, Blinds, Switches are mostly working now

* Code cleanup, fiblary3 req

Fiblary3 is now in pypi, set it as req
Cleanup based on pylint

* Included in .coveragerc and added how to use guide

Included the fibaro component in coveragerc
Added usage instructions to file header

* PyLint inspired fixes

Fixed pylint warnings

* PyLint inspired fixes

PyLint inspired fixes

* updated to fiblary3 0.1.5

* Minor fixes to finally pass pull req

Fixed fiblary3 to work with python 3.5
Updated fiblary3 to 0.1.6
(added energy and batteryLevel dummies)

* module import and flake8 fixes

Finally (hopefully) figured out what lint is complaining about

* Fixed color support for lights, simplified callback

Fixed color support for lights
Simplified callback for updates
Uses updated fiblary3 for color light handling

* Lean and mean refactor

While waiting for a brave reviewer, I've been making the code smaller and easier to understand.

* Minor fixes to please HoundCI

* Removed unused component

Scenes are not implemented yet

* Nicer comments.

* DEVICE_CLASS, ignore plugins, improved mapping

Added support for device class and icons in sensors and binary_sensors
Improved mapping of sensors and added heuristic matching
Added support for hidden devices
Fixed conversion to float in sensors

* Fixed dimming

Fibaro apparently does not need, nor like the extra turnOn commands for dimmers

* flake8

* Cleanup, Light fixes, switch power

Cleanup of the component to separate init from connect, handle connection error better
Improved light handling, especially for RGBW strips and working around Fibaro quirks
Added energy and power reporting to switches

* Missing comment added

Missing comment added to please flake8

* Removed everything but bin.sensors

Stripdown, hoping for a review

* better aligned comments

OMG

* Fixes based on code review

Fixes based on code review

* Implemented stopping

Implemented stopping of StateHandler thread
Cleanup for clarity

* Minor fix

Removed unnecessary list copying

* Nicer wording on shutdown

* Minor changes based on code review

* minor fixes based on code review

* removed extra line break

* Added Fibaro omcponents

Added cover, light, sensor and switch components

* Improved support for Fibaro UBS

Improved support for Fibaro Universal Binary Sensor, when configured to flood sensor or motion sensor.
2018-12-02 10:52:37 +01:00
Adam Mills
a10cbadb57 Restore states when removing/adding entities (#18890) 2018-12-02 10:51:15 +01:00
emontnemery
bbb40fde84 Optionally do not log template rendering errors (#18724) 2018-12-02 10:31:46 +01:00
emontnemery
ce218b172a Small refactoring of MQTT climate (#18814) 2018-12-02 10:30:07 +01:00
emontnemery
2e4e673bbe Small refactoring of MQTT alarm (#18813) 2018-12-02 10:29:31 +01:00
emontnemery
db4a0e3244 Small refactoring of MQTT cover (#18850) 2018-12-02 10:27:50 +01:00
Daniel Høyer Iversen
de82df3c6b Merge pull request #18892 from home-assistant/upgrade-sphinx
Upgrade Sphinx to 1.8.2
2018-12-02 09:57:52 +01:00
Daniel Høyer Iversen
3bc83920b4 Merge branch 'dev' into upgrade-sphinx 2018-12-02 08:46:34 +01:00
Daniel Høyer Iversen
253dc66129 Merge pull request #18895 from home-assistant/upgrade-slacker
Upgrade slacker to 0.11.0
2018-12-02 08:45:02 +01:00
Daniel Høyer Iversen
b063547138 Merge pull request #18851 from emontnemery/mqtt_fan_refactor
Small refactoring of MQTT fan
2018-12-02 08:43:56 +01:00
Daniel Høyer Iversen
d8c6cb1112 Merge pull request #18852 from emontnemery/mqtt_sensor_refactor
Small refactoring of MQTT sensor
2018-12-02 08:40:57 +01:00
Daniel Høyer Iversen
5b0c12b12b Merge pull request #18864 from meatheadmike/dev
Bump pywemo to 0.4.33 (expanded port range fixes dimmers on latest firmware)
2018-12-02 08:38:37 +01:00
Daniel Høyer Iversen
ba372c085c Merge pull request #18880 from ludeeus/tautulli-fix
Fixes error with getting attributes from Tautulli
2018-12-02 08:35:30 +01:00
Daniel Høyer Iversen
2c36f4411e Merge pull request #18879 from ludeeus/multiple-googlehome
Fix stability issues with multiple googlehome units
2018-12-02 08:33:33 +01:00
Daniel Høyer Iversen
8eb9445bea Merge pull request #18904 from home-assistant/upgrade-Pillow
Upgrade pillow to 5.3.0
2018-12-02 08:28:27 +01:00
Daniel Høyer Iversen
456cec2931 Merge pull request #18903 from home-assistant/upgrade-ruamel.yaml
Upgrade ruamel.yaml to 0.15.80
2018-12-02 08:28:04 +01:00
Daniel Høyer Iversen
af7fe8c4fd Merge pull request #18902 from home-assistant/upgrade-restrictedpython
Upgrade restrictedpython to 4.0b7
2018-12-02 08:27:40 +01:00
Daniel Høyer Iversen
41ad04276b Upgrade sphinx-autodoc-typehints to 1.5.1 (#18893) 2018-12-02 08:26:46 +01:00
Fabian Affolter
e591234b59 Upgrade keyring to 17.0.0 (#18901) 2018-12-02 08:26:23 +01:00
Fabian Affolter
9f3c9cdb11 Upgrade pillow to 5.3.0 2018-12-02 00:30:02 +01:00
Fabian Affolter
48b8fc9e01 Upgrade ruamel.yaml to 0.15.80 2018-12-02 00:17:41 +01:00
Fabian Affolter
4807ad7875 Upgrade restrictedpython to 4.0b7 2018-12-02 00:11:47 +01:00
Fabian Affolter
7b6893c9d3 Fix change 2018-12-01 22:08:15 +01:00
Fabian Affolter
4b85ffae4f Upgrade slacker to 0.11.0 2018-12-01 22:01:22 +01:00
Fabian Affolter
2ca4893948 Upgrade sphinx-autodoc-typehints to 1.5.1 2018-12-01 21:48:56 +01:00
Fabian Affolter
9156a827ce Upgrade Sphinx to 1.8.2 2018-12-01 21:45:16 +01:00
ludeeus
1dac84e9dd corrects , -> . typo 2018-12-01 21:34:31 +01:00
ludeeus
da715c2a03 Use dict.get('key') instead of dict['key'] 2018-12-01 21:32:31 +01:00
Fabian Affolter
fc1a4543d3 Fix lint issue 2018-12-01 20:57:39 +01:00
Fabian Affolter
54904fb6c0 Use string formatting 2018-12-01 19:27:21 +01:00
Michael Nosthoff
bd09e96681 Reintroduce unique_id for Netatmo sensor (#18774)
* netatmo: make module type identification more consistent

For the interpretation of voltage values the different types of netatmo
modules need to be distinguished. This is currently done by selecting
the second character of the modules '_id'. The _id-field actually
contains a mac address. This is an undocumented way of identifying the
module_type.

The netatmo API also delivers a field called 'type' which provides a
more consistent way to differentiate the fields. This commit introduces
a differentiation which uses this provided type. This should improve
readability.

Also the field module_id is renamed to module_type which should better
resemble what it actually represents.

* netatmo: reintroduce unique_id using actual module mac address

Each netatmo module features a unique MAC-Address. The base station uses
an actual assigned MAC Address it also uses on the Wifi it connects to.
All other modules have unique MAC Addresses which are only assigned and
used by Netatmo on the internal Wireless-Network. All theses Addresses
are exposed via the API. So we could use the combination
MAC-Address-Sensor_type as unique_id.

In a previous commit this had already been tried but there was a
misunderstanding in what the 'module_id' represented. It was actually
only a module_type representation so it clashed when two modules of the
same type where used.

* Netatmo: fixed line length
2018-12-01 18:00:49 +01:00
ludeeus
8e84401b68 bump ghlocalapi to use clear_scan_result 2018-12-01 16:28:22 +01:00
ludeeus
934eccfeee Fix stability issues with multiple units 2018-12-01 14:55:50 +01:00
ludeeus
89bd6fa494 Fix requirements_all 2018-12-01 14:51:32 +01:00
ludeeus
d8b9bee7fb Fix IndexError for home stats 2018-12-01 14:51:32 +01:00
Joakim Sørensen
558504c686 Fix ordinal filter in template (#18878) 2018-12-01 14:49:34 +01:00
Eliseo Martelli
c69fe43e75 fixed state case for rtorrent (#18778) 2018-12-01 12:00:35 +01:00
Carlos Gustavo Sarmiento
29f15393b1 Updated UVC camera component to support SSL connections (#18829) 2018-12-01 11:58:59 +01:00
Mahasri Kalavala
c23792d1fb Added new filters for templates (#18125)
* added additional filters

Added base64_encode, base64_decode and ordinal filters.

* added test cases

added test cases for base64_encode, base64_decode and ordinal filters.

* forgot to add filters :)
2018-12-01 10:38:10 +01:00
damarco
1ae58ce48b Add support for zha device registry (#18755) 2018-12-01 10:31:49 +01:00
ehendrix23
ecca51b16b Add tests for directv platform (#18590)
* Create test for platform

Created test for platform.
Added media_stop to common.py test

* Multiple improvements

Fixed lint issue in common.py
Fixed lint issues in test_directv.py
Improved patching import using modile_patcher.start() and stop()
Added asserts for service calls.

* Updates based on Martin's review

Updates based on Martin's review.

* Updated test based on PR#18474

Updated test to use service play_media instead of select_source based on change from PR18474

* Lint issues

Lint issues

* Further updates based on feedback

Updates based on feedback provided.

* Using async_load_platform for discovery test

Using async_load_platform for discovery tests.
Added asserts to ensure entities are created with correct names.

* Used HASS event_loop to setup component

Use HASS event_loop to setup the component async.

* Updated to use state machine for # entities

Updated to use state machine to count # entities instead of entities.

* Use hass.loop instead of getting current loop

Small update to use hass.loop instead, thanks Martin!

* Forgot to remove asyncio

Removed asyncio import.

* Added fixtures

Added fixtures.

* Remove not needed updates and assertions

* Return mocked dtv instance from side_effect

* Fix return correct fixture instance

* Clean up assertions

* Fix remaining patches

* Mock time when setting up component in fixture

* Patch time correctly

* Attribute _last_update should return utcnow
2018-12-01 10:28:27 +01:00
Aaron Bach
3a854f4c05 Fix issues with 17track.net sensor names (#18860) 2018-11-30 21:54:40 -07:00
Aaron Bach
c24ddfb1be Bump py17track to 2.1.1 (#18861) 2018-11-30 21:12:55 -07:00
meatheadmike
0754a63969 Bumped pywemo to 0.4.33 2018-11-30 14:03:32 -07:00
meatheadmike
8a75bee82f bump pywemo to 0.4.33
Bump pywemo to 0.4.33 - includes expended port range fix for dimmers
2018-11-30 14:00:26 -07:00
Paulus Schoutsen
df21dd21f2 RFC: Call services directly (#18720)
* Call services directly

* Simplify

* Type

* Lint

* Update name

* Fix tests

* Catch exceptions in HTTP view

* Lint

* Handle ServiceNotFound in API endpoints that call services

* Type

* Don't crash recorder on non-JSON serializable objects
2018-11-30 21:28:35 +01:00
Paulus Schoutsen
bac48aa9d2 Merge pull request #18857 from home-assistant/rc
0.83.2
2018-11-30 20:09:29 +01:00
Paulus Schoutsen
53cbb28926 Fix flaky geofency test (#18855) 2018-11-30 20:06:10 +01:00
Erik Eriksson
d7809c5398 Update of volvooncall component (#18702) 2018-11-30 19:07:42 +01:00
Paulus Schoutsen
9b3373a15b Bumped version to 0.83.2 2018-11-30 17:53:14 +01:00
pbalogh77
474909b515 Hotfix for Fibaro wall plug (#18845)
Fibaro wall plug with a lamp plugged in was misrecognized as a color light, generating crashes in the update function.
2018-11-30 17:53:04 +01:00
Paulus Schoutsen
80f2c2b124 Always set hass_user (#18844) 2018-11-30 17:53:04 +01:00
Darren Foo
ada148eeae bump gtts-token to 1.1.3 (#18824) 2018-11-30 17:53:03 +01:00
emontnemery
449cde5396 Revert change to MQTT discovery_hash introduced in #18169 (#18763) 2018-11-30 17:53:03 +01:00
Paulus Schoutsen
d014517ce2 Always set hass_user (#18844) 2018-11-30 17:32:47 +01:00
pbalogh77
8f50180598 Hotfix for Fibaro wall plug (#18845)
Fibaro wall plug with a lamp plugged in was misrecognized as a color light, generating crashes in the update function.
2018-11-30 17:23:25 +01:00
Erik
1686f73749 Small refactoring of MQTT sensor 2018-11-30 16:53:56 +01:00
Erik
deb9a1133c Small refactoring of MQTT fan 2018-11-30 16:53:14 +01:00
Matt Schmitt
e0f0487ce2 Add services description (#18839) 2018-11-30 16:31:35 +01:00
Daniel Høyer Iversen
44e35ec9a1 update netatmo library (#18823) 2018-11-30 08:45:40 -05:00
emontnemery
a9990c130d Revert change to MQTT discovery_hash introduced in #18169 (#18763) 2018-11-30 13:57:17 +01:00
Darren Foo
fcdb25eb3c bump gtts-token to 1.1.3 (#18824) 2018-11-30 11:18:24 +01:00
Heine Furubotten
4bee3f760f Add Entur departure information sensor (#17286)
* Added Entur departure information sensor.

* Fixed houndci-bot comments.

* Removed tailing whitespace.

* Fixed some comments from tox lint.

* Improved docstring, i think.

* Fix for C1801

* Unit test for entur platform setup

* Rewritten entur component to have pypi dependecy.

* Propper client id for api usage.

* Minor cleanup of usage of constants.

* Made location output configurable.

* Cleaned up usage of constants.

* Moved logic to be contained within setup or update methods.

* Moved icon consts to root in module.

* Using config directly in test

* Minor changes
2018-11-30 09:06:59 +01:00
Andrew Hayworth
5f53627c0a Bump python_awair to 0.0.3 (#18819) 2018-11-30 08:47:05 +01:00
Adam Mills
22f27b8621 Store state last seen time separately (#18806)
* Store state last seen time separately

This ensures that infrequently updated entities aren't accidentally
dropped from the restore states store

* Fix mock restore cache
2018-11-30 08:26:19 +01:00
ehendrix23
a9dc4ba297 Increase pyatv to 0.3.11 (#18801) 2018-11-29 23:44:29 +01:00
Paulus Schoutsen
3701c0f219 Merge pull request #18811 from home-assistant/rc
0.83.1
2018-11-29 23:18:13 +01:00
ehendrix23
a035725c67 Service already discovered log entry (#18800)
Add debug log entry if service is already discovered.
2018-11-29 23:15:48 +01:00
Paulus Schoutsen
440614dd9d Use proper signals (#18613)
* Emulated Hue not use deprecated handler

* Remove no longer needed workaround

* Add middleware directly

* Dont always load the ban config file

* Update homeassistant/components/http/ban.py

Co-Authored-By: balloob <paulus@home-assistant.io>

* Update __init__.py
2018-11-29 23:05:23 +01:00
Paulus Schoutsen
163c881ced Bumped version to 0.83.1 2018-11-29 22:58:06 +01:00
pbalogh77
0467d0563a Hotfix for crash with virtual devices (#18808)
* Quickfix for crash with virtual devices

Added try/except to critical loops of processing
Reinforced read_devices, map_device_to_type and update processing

* oops
2018-11-29 22:57:45 +01:00
pbalogh77
2b52f27eb9 Hotfix for crash with virtual devices (#18808)
* Quickfix for crash with virtual devices

Added try/except to critical loops of processing
Reinforced read_devices, map_device_to_type and update processing

* oops
2018-11-29 22:57:05 +01:00
Aaron Bach
31d7221c90 Remove additional self from update function in RainMachine (#18810) 2018-11-29 22:51:16 +01:00
Daniel Høyer Iversen
d9124b182a Remove self from update function in rainmachine (#18807) 2018-11-29 22:51:15 +01:00
Aaron Bach
f2b818658f Bumped py17track to 2.1.0 (#18804) 2018-11-29 22:51:15 +01:00
Eric Nagley
5a6ac9ee72 BUGFIX: handle extra fan speeds. (#18799)
* BUGFIX: add support for extra fan speeds.

* Drop extra fan speeds.

Remove catch all, drop missing fan speeds.

* fix self.speed_synonyms call. Remove un-needed keys() call
2018-11-29 22:51:14 +01:00
Paulus Schoutsen
7fa5f07218 Fix race condition in group.set (#18796) 2018-11-29 22:51:13 +01:00
Paulus Schoutsen
fa9a200e3c Render the secret (#18793) 2018-11-29 22:51:13 +01:00
Paulus Schoutsen
0ca67bf6f7 Make auth backwards compat again (#18792)
* Made auth not backwards compat

* Fix tests
2018-11-29 22:51:12 +01:00
cdce8p
f1c5e756ff Fix logbook domain filter - alexa, homekit (#18790) 2018-11-29 22:51:12 +01:00
Paulus Schoutsen
ff33d34b81 Legacy api fix (#18733)
* Set user for API password requests

* Fix tests

* Fix typing
2018-11-29 22:51:11 +01:00
Ian Richardson
601389302a Convert shopping-list update to WebSockets (#18713)
* Convert shopping-list update to WebSockets

* Update shopping_list.py

* Update test_shopping_list.py
2018-11-29 22:51:10 +01:00
Ian Richardson
2ba521caf8 Add websocket call for adding item to shopping-list (#18623) 2018-11-29 22:51:10 +01:00
Aaron Bach
6f7ff9a18a Remove additional self from update function in RainMachine (#18810) 2018-11-29 14:47:41 -07:00
Daniel Høyer Iversen
4bc9e6dfe0 Remove self from update function in rainmachine (#18807) 2018-11-29 22:28:27 +01:00
Paulus Schoutsen
28215d7edd Make auth backwards compat again (#18792)
* Made auth not backwards compat

* Fix tests
2018-11-29 22:26:19 +01:00
Paulus Schoutsen
38ecf71307 Fix race condition in group.set (#18796) 2018-11-29 22:26:06 +01:00
Eric Nagley
4e272624eb BUGFIX: handle extra fan speeds. (#18799)
* BUGFIX: add support for extra fan speeds.

* Drop extra fan speeds.

Remove catch all, drop missing fan speeds.

* fix self.speed_synonyms call. Remove un-needed keys() call
2018-11-29 22:24:53 +01:00
Aaron Bach
ab4d0a7fc3 Bumped py17track to 2.1.0 (#18804) 2018-11-29 22:24:32 +01:00
Paulus Schoutsen
ca74f5efde Render the secret (#18793) 2018-11-29 22:17:01 +01:00
Eric Nagley
e50a6ef8af Add support for Mode trait in Google Assistant. (#18772)
* Add support for Mode trait in Google Assistant.

* Simplify supported logic.

* Fix SUPPORTED_MODE_SETTINGS to correct rip failures.

* more stray commas

* update tests.
2018-11-29 21:14:17 +01:00
Eliseo Martelli
5c026b1fa2 Added qbittorrent sensor platform (#18618)
* added qbittorrent sensor platform

* Added requirements

* linting

* disabled broad-except

* added noqa

* removed pass statement (left that from development session)

* Added to coveragerc & moved to async

* fixed linting

* fixed indentation

* removed white space

* added await

* Removed generic exception

* removed pylint disable

* added auth checks

* linting

* fixed linting

* fixed error

* should be fixed now

* linting

* ordered imports

* added requested changes

* Update homeassistant/components/sensor/qbittorrent.py

Co-Authored-By: eliseomartelli <martely98@gmail.com>

* Update qbittorrent.py

* Minor changes
2018-11-29 20:40:26 +01:00
cdce8p
474567e762 Fix logbook domain filter - alexa, homekit (#18790) 2018-11-29 20:16:39 +01:00
Fabian Affolter
16911a5cb4 Update lang list (fixes #18768) (#18773)
* Update lang list (fixes #18768)

* Fix lint issues
2018-11-29 19:14:14 +01:00
Daniel Høyer Iversen
46389fb6ca Update switchmate lib (#18785) 2018-11-29 19:13:08 +01:00
Paulus Schoutsen
9aeb489282 Raise NotImplementedError (#18777) 2018-11-29 16:40:49 +01:00
Daniel Høyer Iversen
8c9a39845c Round average price for Tibber (#18784) 2018-11-29 16:39:39 +01:00
Fabian Affolter
c976ac3b39 Fix lint issues 2018-11-29 12:28:50 +01:00
mdallaire
07a7ee0ac7 Add more waterfurnace sensors (#18451)
Add the following sensors that provide interesting data when using a variable speed geothermal system:

* Compressor Power
* Fan Power
* Aux Power
* Loop Pump Power
* Compressor Speed
* Fan Speed
2018-11-29 06:04:12 -05:00
Paulus Schoutsen
c6c55c4419 Merge pull request #18776 from home-assistant/rc
0.83
2018-11-29 11:45:20 +01:00
Paulus Schoutsen
1364114dc1 Bumped version to 0.83.0 2018-11-29 10:57:40 +01:00
Ian Richardson
a306475065 Convert shopping-list clear to WebSockets (#18769) 2018-11-29 10:06:18 +01:00
Fabian Affolter
faeaa43393 Update lang list (fixes #18768) 2018-11-29 09:26:48 +01:00
ehendrix23
aadf72d445 Fix statistics for binary sensor (#18764)
* Fix statistics for binary sensor

-) Binary sensors have 'on' and 'off' for state resulting in issue as numbers were expected. Fixed so that it works with non-numeric states as well.
-) Added check to skip unknown states.
-) Updates test so that binary sensor test will use non-numeric values for states.

* Using guard clause and changed debug to error

Changed to use a guard clause for state unknown.
Writing error on value error instead of debug.

* Add docstring
2018-11-29 09:01:56 +01:00
Paulus Schoutsen
05915775e3 Bumped version to 0.83.0b3 2018-11-28 22:47:37 +01:00
Paulus Schoutsen
311c796da7 Default to on if logged in (#18766) 2018-11-28 22:47:09 +01:00
Paulus Schoutsen
f860cac4ea OwnTracks Config Entry (#18759)
* OwnTracks Config Entry

* Fix test

* Fix headers

* Lint

* Username for android only

* Update translations

* Tweak translation

* Create config entry if not there

* Update reqs

* Types

* Lint
2018-11-28 22:47:08 +01:00
Adam Mills
58e0ff0b1b Async tests for owntracks device tracker (#18681) 2018-11-28 22:47:08 +01:00
Paulus Schoutsen
48e28843e6 OwnTracks Config Entry (#18759)
* OwnTracks Config Entry

* Fix test

* Fix headers

* Lint

* Username for android only

* Update translations

* Tweak translation

* Create config entry if not there

* Update reqs

* Types

* Lint
2018-11-28 22:20:13 +01:00
Paulus Schoutsen
e06fa0d2d0 Default to on if logged in (#18766) 2018-11-28 22:17:37 +01:00
ehendrix23
0bdf96d94c Add block after setting up component (#18756)
Added a block_till_done after setting up component and before starting HASS.
2018-11-28 16:14:37 +01:00
Fabian Affolter
623cec206b Upgrade Adafruit-DHT to 1.4.0 (fixes #15847) (#18614) 2018-11-28 13:38:26 +01:00
Paulus Schoutsen
a2386f871d Forbid float NaN in JSON (#18757) 2018-11-28 13:25:23 +01:00
Adam Mills
5c3a4e3d10 Restore states through a JSON store instead of recorder (#17270)
* Restore states through a JSON store

* Accept entity_id directly in restore state helper

* Keep states stored between runs for a limited time

* Remove warning
2018-11-28 13:16:43 +01:00
Diogo Gomes
a039c3209b Replace token in camera.push with webhook (#18380)
* replace token with webhook

* missing PR 18206 aditions

* remove unused property

* increase robustness

* lint

* address review comments

* id -> name
2018-11-28 10:36:29 +01:00
majuss
fc8b1f4968 Update lupupy version to 0.0.13 (#18754)
* lupupy version push
2018-11-27 21:21:27 -05:00
damarco
052d305243 Add config entry for ZHA (#18352)
* Add support for zha config entries

* Add support for zha config entries

* Fix node_config retrieval

* Dynamically load discovered entities

* Restore device config support

* Refactor loading of entities

* Remove device registry support

* Send discovery_info directly

* Clean up discovery_info in hass.data

* Update tests

* Clean up rebase

* Simplify config flow

* Address comments

* Fix config path and zigpy check timeout

* Remove device entities when unloading config entry
2018-11-27 21:21:25 +01:00
ehendrix23
43676fcaf4 Moved stop method and registering STOP_EVENT outside of init (#18582)
* Moved stop method and registering outside of init

Moved the cleanup to a seperate method and perform registering for the event in setup.

* Removed use of global variable

Removed use of global variable.

* Removed API_SESSIONS

Removed unused declaration API_SESSIONS.
2018-11-27 20:41:25 +01:00
Paulus Schoutsen
f3047b9c03 Fix logbook filtering entities (#18721)
* Fix logbook filtering entities

* Fix flaky test
2018-11-27 20:16:32 +01:00
Paulus Schoutsen
775c909a8c Bumped version to 0.83.0b2 2018-11-27 20:15:57 +01:00
Paulus Schoutsen
3a8303137a Add permission checks to Rest API (#18639)
* Add permission checks to Rest API

* Clean up unnecessary method

* Remove all the tuple stuff from entity check

* Simplify perms

* Correct param name for owner permission

* Hass.io make/update user to be admin

* Types
2018-11-27 20:15:48 +01:00
Aaron Bach
093fa6f5e9 Bumped simplisafe-python to 3.1.14 (#18752) 2018-11-27 11:40:49 -07:00
Anton Johansson
dd8544fdf8 Fix typo in log (#18751) 2018-11-27 13:09:25 -05:00
Bryan York
02309cc318 Enable Google Assistant OnOffTrait for climate devices that support them (#18544)
* Enable Google Assistant OnOffTrait for climate devices that support them

This commit enables the OnOffTrait for climate devices that have the SUPPORT_ON_OFF feature. I have tested this locally with a Sensibo device which supports ON_OFF and a nest device that does not.

* Update trait.py

* Add tests for onoff_climate

* Add OnOff trait to climate.heatpump

* Add on status to heatpump in google_assistant tests
2018-11-27 17:11:55 +01:00
Austin
2f07e92cc2 Fix decora_wifi residences (#17228)
* Fix decora multiple residences

* Fix typo

* Update decora_wifi.py
2018-11-27 16:53:28 +01:00
Luis Martinez de Bartolome Izquierdo
7b3b7d2eec Wunderlist component (#18339)
* Wunderlist component

* Check credentials

* Dont print credentials

* Update __init__.py
2018-11-27 15:44:09 +01:00
Fredrik Erlandsson
5d5c78b374 Add unique_id for Daikin entities (#18747) 2018-11-27 15:36:55 +01:00
Fredrik Erlandsson
eb2e2a116e Add unique_id for tellduslive (#18744) 2018-11-27 15:35:51 +01:00
Fredrik Erlandsson
392898e694 Updated codeowners (#18746) 2018-11-27 14:59:25 +01:00
Jason Hu
4d5338a1b0 Fix google assistant request sync service call (#17415)
* Update __init__.py

* Add optional agent_user_id field to request_sync service

* Update services.yaml
2018-11-27 14:57:42 +01:00
kennedyshead
87507c4b6f fix aioasuswrt sometimes return empty lists (#18742)
* aioasuswrt sometimes return empty lists

* Bumping aioasuswrt to 1.1.12
2018-11-27 08:20:25 -05:00
Luis Martinez de Bartolome Izquierdo
9d1b94c24a Supports the new Netatmo Home Coach (#18308)
* Supports the new Netatmo Home Coach

* unused import

* Missing docstring

* Fixed pylint

* pydocs

* doc style
2018-11-27 14:01:34 +01:00
emontnemery
16e3ff2fec Mqtt light refactor (#18227)
* Rename mqtt light files

* Refactor mqtt light

* Remove outdated testcase

* Add backwards compatibility for MQTT discovered MQTT lights.
Refactor according to review comments.
2018-11-27 14:00:05 +01:00
Robert Dunmire III
c1ed2f17ac Update librouteros and re-connect to api if connection is lost (#18421)
* Reconnect when connection is lost

* Fix tabs

* add librouteros.exceptions

* add logger

* fix line too long

* added import librouteros

* Update librouteros version

* Update mikrotik.py

* Update mikrotik.py

* Fix trailing whitespace

* Update mikrotik.py

* Update mikrotik.py
2018-11-27 13:26:52 +01:00
Fabian Affolter
1cbe080df9 Fix remaining issues (#18416) 2018-11-27 13:21:42 +01:00
Malte Franken
61e0e11156 Geo Location platform code clean up (#18717)
* code cleanup to make use of new externalised feed manager

* fixed lint

* revert change, keep asynctest

* using asynctest

* changed unit test from mocking to inspecting dispatcher signals

* code clean-up
2018-11-27 13:12:29 +01:00
Malte Franken
013e181497 U.S. Geological Survey Earthquake Hazards Program Feed platform (#18207)
* new platform for usgs earthquake hazards program feed

* lint and pylint issues

* fixed config access

* shortened names of platform, classes, etc.

* refactored tests

* fixed hound

* regenerated requirements

* refactored tests

* fixed hound
2018-11-27 12:55:15 +01:00
David Bonnes
9a25054a0d Add zones to evohome component (#18428)
* Added Zones, and removed available() logic

flesh out Zones

tidy up init

some more tidying up

Nearly there - full functionality

passed txo - ready to send PR

Ready to PR, except to remove logging

Add Zones and associated functionality to evohome component

Add Zones to evohome (some more tidying up)

Add Zones to evohome (Nearly there - full functionality)

Add Zones to evohome (passed tox)

Add Zones to evohome (except to remove logging)

Add Zones and associated functionality to evohome component

Revert _LOGGER.warn to .debug, as it should be

Cleanup stupid REBASE

* removed a duplicate/unwanted code block

* tidy up comment

* use async_added_to_hass instead of bus.listen

* Pass evo_data instead of hass when instntiating

* switch to async version of setup_platform/add_entities

* Remove workaround for bug in client library
 - using github version for now, as awaiting new PyPi package

* Avoid invalid-name lint - use 'zone_idx' instead of 'z'

* Fix line too long error

* remove commented-out line of code

* fix a logic error, improve REDACTION of potentially-sensitive infomation

* restore use of EVENT_HOMEASSISTANT_START to improve HA startup time

* added a docstring to _flatten_json

* Switch instantiation from component to platform

* Use v0.2.8 of client api (resolves logging bug)

* import rather than duplicate, and de-lint

* We use evohomeclient v0.2.8 now

* remove all the api logging

* Changed scan_interal to Throttle

* added a configurable scan_interval

* small code tidy-up, removed sub-function

* tidy up update() code

* minimize use of self.hass.data[]

* remove lint

* remove unwanted logging

* remove debug code

* correct a small coding error

* small tidyup of code

* remove flatten_json

* add @callback to _first_update()

* switch back to load_platform

* adhere to standards fro logging

* use new format string formatting

* minor change to comments

* convert scan_interval to timedelta from int

* restore rounding up of scan_interval

* code tidy up

* sync when in sync context

* fix typo

* remove raises not needed

* tidy up typos, etc.

* remove invalid-name lint

* tidy up exception handling

* de-lint/pretty-fy

* move 'status' to a JSON node, so theirs room for 'config', 'schedule' in the future
2018-11-27 12:17:22 +01:00
emontnemery
a03cb12c61 Reconfigure MQTT sensor component if discovery info is changed (#18178)
* Reconfigure MQTT sensor component if discovery info is changed

* Do not pass hass to MqttSensor constructor

* Remove duplicated line
2018-11-27 11:23:47 +01:00
emontnemery
4a4ed128db Reconfigure MQTT fan component if discovery info is changed (#18177) 2018-11-27 11:22:55 +01:00
emontnemery
6170065a2c Reconfigure MQTT cover component if discovery info is changed (#18175)
* Reconfigure MQTT cover component if discovery info is changed

* Do not pass hass to MqttCover constructor
2018-11-27 11:22:26 +01:00
Matt Hamilton
4f2e7fc912 remove pbkdf2 upgrade path (#18736) 2018-11-27 10:42:56 +01:00
Paulus Schoutsen
c2f8dfcb9f Legacy api fix (#18733)
* Set user for API password requests

* Fix tests

* Fix typing
2018-11-27 10:41:44 +01:00
Paulus Schoutsen
9d7b1fc3a7 Enforce permissions for Websocket API (#18719)
* Handle unauth exceptions in websocket

* Enforce permissions in websocket API
2018-11-27 10:12:31 +01:00
Ville Skyttä
7248c9cb0e Remove some unused imports (#18732) 2018-11-27 09:35:35 +01:00
Ville Skyttä
b4e2f2a6ef Upgrade pytest and -timeout (#18722)
* Upgrade pytest to 4.0.1

* Upgrade pytest-timeout to 1.3.3
2018-11-26 22:43:14 +01:00
Paulus Schoutsen
9894eff732 Fix logbook filtering entities (#18721)
* Fix logbook filtering entities

* Fix flaky test
2018-11-26 19:53:24 +01:00
Paulus Schoutsen
1f123ebcc1 Updated frontend to 20181126.0 2018-11-26 14:40:43 +01:00
Paulus Schoutsen
3c92aa9ecb Update translations 2018-11-26 14:30:21 +01:00
Paulus Schoutsen
f9f71c4a6d Bumped version to 0.83.0b1 2018-11-26 14:20:56 +01:00
Joakim Sørensen
c3b76b40f6 Set correct default offset (#18678) 2018-11-26 14:20:49 +01:00
Bram Kragten
56c7c8ccc5 Fix vol Dict -> dict (#18637) 2018-11-26 14:20:49 +01:00
Fredrik Erlandsson
bb75a39cf1 Updated webhook_register, version bump pypoint (#18635)
* Updated webhook_register, version bump pypoint

* A binary_sensor should be a BinarySensorDevice
2018-11-26 14:20:48 +01:00
Eliseo Martelli
2f581b1a1e fixed wording that may confuse user (#18628) 2018-11-26 14:20:48 +01:00
pbalogh77
cf22060c5e Use asyncio Lock for fibaro light (#18622)
* Use asyncio Lock for fibaro light

* line length and empty line at end

* async turn_off

Turned the turn_off into async as well

* bless you, blank lines...

My local flake8 lies to me. Not cool.
2018-11-26 14:20:47 +01:00
Paulus Schoutsen
6bcedb3ac5 Updated frontend to 20181121.1 2018-11-26 14:16:30 +01:00
Paulus Schoutsen
7848381f43 Allow managing cloud webhook (#18672)
* Add cloud webhook support

* Simplify payload

* Add cloud http api tests

* Fix tests

* Lint

* Handle cloud webhooks

* Fix things

* Fix name

* Rename it to cloudhook

* Final rename

* Final final rename?

* Fix docstring

* More tests

* Lint

* Add types

* Fix things
2018-11-26 14:10:18 +01:00
pbalogh77
4a661e351f Use asyncio Lock for fibaro light (#18622)
* Use asyncio Lock for fibaro light

* line length and empty line at end

* async turn_off

Turned the turn_off into async as well

* bless you, blank lines...

My local flake8 lies to me. Not cool.
2018-11-26 13:17:56 +01:00
Ian Richardson
b5b5bc2de8 Convert shopping-list update to WebSockets (#18713)
* Convert shopping-list update to WebSockets

* Update shopping_list.py

* Update test_shopping_list.py
2018-11-26 09:59:53 +01:00
emontnemery
d290ce3c9e Small refactoring of MQTT binary_sensor (#18674) 2018-11-25 20:53:03 +01:00
Franck Nijhof
2cbe083460 ⬆️ Upgrades InfluxDB dependency to 5.2.0 (#18668) 2018-11-25 20:52:09 +01:00
Paulus Schoutsen
8b8629a5f4 Add permission checks to Rest API (#18639)
* Add permission checks to Rest API

* Clean up unnecessary method

* Remove all the tuple stuff from entity check

* Simplify perms

* Correct param name for owner permission

* Hass.io make/update user to be admin

* Types
2018-11-25 18:04:48 +01:00
Fabian Affolter
f387cdec59 Upgrade pysnmp to 4.4.6 (#18695) 2018-11-25 17:59:14 +01:00
Adam Mills
78b90be116 Async cover template tests (#18690) 2018-11-25 11:39:35 -05:00
Adam Mills
91c526d9fe Async device sun light trigger tests (#18689) 2018-11-25 11:39:18 -05:00
Jens
f3ce463862 Adds SomfyContactIOSystemSensor to TaHoma (#18560)
* Sorts all TAHOME_TYPES and adds SomfyContactIOSystemSensor as it wasn't added with 558b659f7c

* Fixes syntax errors related to sorting of entries.
2018-11-25 13:47:16 +01:00
Joakim Sørensen
23f5d785c4 Set correct default offset (#18678) 2018-11-25 12:30:38 +01:00
Soós Péter
cd773455f0 Fix false log message on CAPsMAN only devices (#18687)
* Fix false log message on CAPsMAN only devices 

False debug log message appeared on CAPsMAN only devices without physichal wireless interfaces. This fix eliminates them.

* Fixed indentation to pass flake8 test
2018-11-25 12:21:26 +01:00
Fabian Affolter
5a5cbe4e72 Upgrade youtube_dl to 2018.11.23 (#18694) 2018-11-25 11:41:49 +01:00
Daniel Høyer Iversen
ad2e8b3174 update mill lib, handle bad data from mill server (#18693) 2018-11-25 09:39:06 +01:00
Andrew Hayworth
eb6b6ed87d Add Awair sensor platform (#18570)
* Awair Sensor Platform

This commit adds a sensor platform for Awair devices, by accessing
their beta API. Awair heavily rate-limits this API, so we throttle
updates based on the number of devices found. We also allow for the
user to bypass API device listing entirely, because the device list
endpoint is limited to only 6 calls per day. A crashing or restarting
server would quickly hit that limit.

This sensor platform uses the python_awair library (also written
as part of this PR), which is available for async usage.

* Disable pylint warning for broad try/catch

It's true that this is generally not a great idea, but we really don't
want to crash here. If we can't set up the platform, logging it and
continuing is the right answer.

* Add space to satisfy the linter

* Awair platform PR feedback

- Bump python_awair to 0.0.2, which has support for more granular exceptions
- Ensure we have python_awair available in test
- Raise PlatformNotReady if we can't set up Awair
- Make the 'Awair score' its own sensor, rather than exposing it other ways
- Set the platform up as polling, and set a sensible default
- Pass in throttling parameters to the underlying data class, rather
than use hacky global variable access to dynamically set the interval
- Switch to dict access for required variables
- Use pytest coroutines, set up components via async_setup_component,
  and test/modify/assert in generally better ways
- Commit test data as fixtures

* Awair PR feedback, volume 2

- Don't force updates in test, instead modify time itself and let
  homeassistant update things "normally".
- Remove unneeded polling attribute
- Rename timestamp attribute to 'last_api_update', to better reflect
  that it is the timestamp of the last time the Awair API servers
  received data from this device.
- Use that attribute to flag the component as unavailable when data
  is stale. My own Awair device periodically goes offline and it really
  hardly indicates that at all.
- Dynamically set fixture timestamps to the test run utcnow() value,
  so that we don't have to worry about ancient timestamps in tests
  blowing up down the line.
- Don't assert on entities directly, for the most part. Find desired
  attributes in ... the attributes dict.

* Patch an instance of utcnow I overlooked

* Switch to using a context manager for timestream modification

Honestly, it's just a lot easier to keep track of patches. Moreover,
the ones I seem to have missed are now caught, and tests seem to
consistently pass.

Also, switch test_throttle_async_update to manipulating time more
explicitly.

* Missing blank line, thank you hound

* Fix pydocstyle error

I very much need to set up a script to do this quickly w/o tox, because
running flake8 is not enough!

* PR feedback

* PR feedback
2018-11-25 09:01:19 +01:00
Adam Mills
00c9ca64c8 Async tests for mqtt switch (#18685) 2018-11-24 17:08:28 -05:00
Adam Mills
6f0a3b4b22 Async tests for counter (#18684) 2018-11-24 16:12:29 -05:00
Adam Mills
66f1643de5 Async timer tests (#18683) 2018-11-24 16:12:19 -05:00
Adam Mills
50a30d4dc9 Async tests for remaining device trackers (#18682) 2018-11-24 15:10:57 -05:00
Adam Mills
6ebdc7dabc Async tests for owntracks device tracker (#18681) 2018-11-24 14:34:36 -05:00
Adam Mills
d24ea7da90 Async tests for device tracker mqtt (#18680) 2018-11-24 13:24:06 -05:00
emontnemery
5e18d52302 Reconfigure MQTT alarm component if discovery info is changed (#18173) 2018-11-24 10:48:01 +01:00
emontnemery
e41af133fc Reconfigure MQTT climate component if discovery info is changed (#18174) 2018-11-24 10:40:07 +01:00
Bram Kragten
986ca23934 Dict -> dict (#18665) 2018-11-24 10:02:06 +01:00
Kacper Krupa
8771f9f7dd converted majority of effects from ifs to dict map, which makes it easier to extend in the future. Also, added LSD effect! (#18656) 2018-11-23 23:53:33 +01:00
Bram Kragten
37327f6cbd Add save command to lovelace (#18655)
* Add save command to lovelace

* Default for save should by json

* typing
2018-11-23 22:56:58 +01:00
emontnemery
4c04abfccc Merge pull request #18654 from emontnemery/fix_mqtt_availability_qos
Support updated MQTT QoS when reconfiguring MQTT availability
2018-11-23 17:13:29 +01:00
Erik
b198bb441a Support updated MQTT QoS when reconfiguring MQTT availability 2018-11-23 15:32:24 +01:00
Eliseo Martelli
1c17b885db Added deviceclass timestamp constant (#18652)
* Added deviceclass timestamp

* added device class timestamp to sensor

* fixed comment
2018-11-23 14:51:26 +01:00
Paulus Schoutsen
c0cf29aba9 Remove since last boot from systemmonitor sensor (#18644)
* Remove since last boot

* Make systemmonitor last_boot be a timestamp
2018-11-23 11:55:45 +01:00
Ian Richardson
92978b2f26 Add websocket call for adding item to shopping-list (#18623) 2018-11-23 08:56:18 +01:00
Adam Mills
c99204149c Convert device tracker init tests to async (#18640) 2018-11-23 08:55:25 +01:00
cdheiser
98f159a039 [Breaking Change] Cleanup Lutron light component (#18650)
Remove the return value from setup_platform
Convert LutronLight.__init__ to use super() when referencing the parent class.
Change device_state_attributes() to use lowercase snakecase (Rename 'Lutron Integration ID' to 'lutron_integration_id')
2018-11-23 08:54:28 +01:00
Eliseo Martelli
bb37151987 fixed wording that may confuse user (#18628) 2018-11-23 01:46:22 +01:00
Diogo Gomes
af0f3fcbdb IPMA Weather Service - version bump (#18626)
* version bump

* gen

* gen
2018-11-22 19:09:45 -05:00
Joakim Sørensen
c7bfdbf3cf Merge pull request #18632 from dshokouhi/neato_additional_error_messages
Add additional neato error messages to status attribute
2018-11-22 22:20:58 +01:00
David F. Mulcahey
67aa76d295 Refactor ZHA (#18629)
* refactor ZHA

* lint

* review request

* Exclude more zha modules from coverage
2018-11-22 19:00:46 +01:00
Fredrik Erlandsson
cccc41c23e Updated webhook_register, version bump pypoint (#18635)
* Updated webhook_register, version bump pypoint

* A binary_sensor should be a BinarySensorDevice
2018-11-22 16:43:10 +01:00
Pascal Vizeli
13144af65e Fix raising objects on proxy camera component 2018-11-22 15:06:31 +01:00
Giuseppe
b246fc977e Add support for cropping pictures in proxy camera (#18431)
* Added support for cropping pictures in proxy camera

This includes extending the configuration to introduce a mode
(either 'resize', default, or 'crop') and further coordinates
for the crop operation.

* Also fixed async job type, following code review
2018-11-22 13:14:28 +01:00
Bram Kragten
e5d2900151 Fix vol Dict -> dict (#18637) 2018-11-22 12:48:50 +01:00
mopolus
01ee03a9a1 Add support for multiple IHC controllers (#18058)
* Added support for secondary IHC controller

Most IHC systems only have one controller but the system can be setup with a linked secondary controller.
I have updated the code to have it support both primary and secondary controller. Existing configuration is not impacted and secondary controller can be setup the same way, with similar settings nested under 'secondary' in the configuration

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update ihc.py

* Update __init__.py

* Update const.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update const.py

* Update __init__.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update __init__.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update ihc.py

* Update __init__.py

* Update __init__.py

* Update ihc.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

indentation was incorrect for "load_platform" in "get_manual_configuration". Load_platform was not called with the correct component name
2018-11-22 09:45:40 +01:00
Daniel Shokouhi
7daf2caef2 Correct error message 2018-11-21 23:31:08 -08:00
Daniel Shokouhi
9f36cebe59 Add additional neato error messages to status attribute 2018-11-21 22:47:30 -08:00
Nick Whyte
22ab83acae Cleanup BOM dependencies + add basic test + IDEA autoformat (#18462)
* Cleanup BOM dependencies + add basic test
2018-11-21 20:41:53 -05:00
nragon
1ad3c3b1e2 Minor change to still image on mjpeg (#18602)
* Update mjpeg.py

* Lint
2018-11-21 23:12:16 +01:00
Josh Anderson
3d178708fc Add /sbin to launchd PATH (#18601)
* Add /sbin to launchd PATH

* Put /sbin at the end to allow overrides

Co-Authored-By: andersonshatch <andersonshatch@gmail.com>
2018-11-21 20:56:38 +01:00
Paulus Schoutsen
1341ecd2eb Use proper signals (#18613)
* Emulated Hue not use deprecated handler

* Remove no longer needed workaround

* Add middleware directly

* Dont always load the ban config file

* Update homeassistant/components/http/ban.py

Co-Authored-By: balloob <paulus@home-assistant.io>

* Update __init__.py
2018-11-21 20:55:21 +01:00
Paulus Schoutsen
5b3e9399a9 Bump to 0.84.0.dev0 2018-11-21 20:53:44 +01:00
504 changed files with 17031 additions and 10177 deletions

View File

@@ -148,6 +148,9 @@ omit =
homeassistant/components/hive.py
homeassistant/components/*/hive.py
homeassistant/components/hlk_sw16.py
homeassistant/components/*/hlk_sw16.py
homeassistant/components/homekit_controller/__init__.py
homeassistant/components/*/homekit_controller.py
@@ -203,6 +206,9 @@ omit =
homeassistant/components/linode.py
homeassistant/components/*/linode.py
homeassistant/components/lightwave.py
homeassistant/components/*/lightwave.py
homeassistant/components/logi_circle.py
homeassistant/components/*/logi_circle.py
@@ -323,7 +329,8 @@ omit =
homeassistant/components/tahoma.py
homeassistant/components/*/tahoma.py
homeassistant/components/tellduslive.py
homeassistant/components/tellduslive/__init__.py
homeassistant/components/tellduslive/entry.py
homeassistant/components/*/tellduslive.py
homeassistant/components/tellstick.py
@@ -400,6 +407,8 @@ omit =
homeassistant/components/zha/__init__.py
homeassistant/components/zha/const.py
homeassistant/components/zha/entities/*
homeassistant/components/zha/helpers.py
homeassistant/components/*/zha.py
homeassistant/components/zigbee.py
@@ -637,7 +646,6 @@ omit =
homeassistant/components/notify/group.py
homeassistant/components/notify/hipchat.py
homeassistant/components/notify/homematic.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py
@@ -780,6 +788,7 @@ omit =
homeassistant/components/sensor/pushbullet.py
homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/pyload.py
homeassistant/components/sensor/qbittorrent.py
homeassistant/components/sensor/qnap.py
homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py

View File

@@ -51,6 +51,7 @@ 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/binary_sensor/threshold.py @fabaff
homeassistant/components/binary_sensor/uptimerobot.py @ludeeus
homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50
homeassistant/components/climate/eq3btsmart.py @rytilahti
@@ -61,9 +62,11 @@ homeassistant/components/cover/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/asuswrt.py @kennedyshead
homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/googlehome.py @ludeeus
homeassistant/components/device_tracker/huawei_router.py @abmantis
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/device_tracker/traccar.py @ludeeus
homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme
homeassistant/components/history_graph.py @andrey-git
homeassistant/components/influx.py @fabaff
@@ -109,6 +112,7 @@ homeassistant/components/sensor/glances.py @fabaff
homeassistant/components/sensor/gpsd.py @fabaff
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/jewish_calendar.py @tsvi
homeassistant/components/sensor/launch_library.py @ludeeus
homeassistant/components/sensor/linux_battery.py @fabaff
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/min_max.py @fabaff
@@ -119,6 +123,7 @@ homeassistant/components/sensor/pi_hole.py @fabaff
homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/pvoutput.py @fabaff
homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/ruter.py @ludeeus
homeassistant/components/sensor/scrape.py @fabaff
homeassistant/components/sensor/serial.py @fabaff
homeassistant/components/sensor/seventeentrack.py @bachya
@@ -128,12 +133,15 @@ homeassistant/components/sensor/sql.py @dgomes
homeassistant/components/sensor/statistics.py @fabaff
homeassistant/components/sensor/swiss*.py @fabaff
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tautulli.py @ludeeus
homeassistant/components/sensor/time_data.py @fabaff
homeassistant/components/sensor/version.py @fabaff
homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/sensor/worldclock.py @fabaff
homeassistant/components/shiftr.py @fabaff
homeassistant/components/spaceapi.py @fabaff
homeassistant/components/switch/switchbot.py @danielhiversen
homeassistant/components/switch/switchmate.py @danielhiversen
homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/vacuum/roomba.py @pschmitt
homeassistant/components/weather/__init__.py @fabaff
@@ -157,9 +165,12 @@ homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen
# C
homeassistant/components/cloudflare.py @ludeeus
homeassistant/components/counter/* @fabaff
# D
homeassistant/components/daikin.py @fredrike @rofrantz
homeassistant/components/*/daikin.py @fredrike @rofrantz
homeassistant/components/*/deconz.py @kane610
homeassistant/components/digital_ocean.py @fabaff
homeassistant/components/*/digital_ocean.py @fabaff
@@ -204,6 +215,10 @@ homeassistant/components/*/mystrom.py @fabaff
homeassistant/components/openuv/* @bachya
homeassistant/components/*/openuv.py @bachya
# P
homeassistant/components/point/* @fredrike
homeassistant/components/*/point.py @fredrike
# Q
homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza
@@ -221,8 +236,8 @@ homeassistant/components/*/simplisafe.py @bachya
# T
homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/tellduslive/*.py @fredrike
homeassistant/components/*/tellduslive.py @fredrike
homeassistant/components/tesla.py @zabuldon
homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/thethingsnetwork.py @fabaff

View File

@@ -78,11 +78,6 @@ class AuthManager:
hass, self._async_create_login_flow,
self._async_finish_login_flow)
@property
def active(self) -> bool:
"""Return if any auth providers are registered."""
return bool(self._providers)
@property
def support_legacy(self) -> bool:
"""
@@ -132,13 +127,15 @@ class AuthManager:
return None
async def async_create_system_user(self, name: str) -> models.User:
async def async_create_system_user(
self, name: str,
group_ids: Optional[List[str]] = None) -> models.User:
"""Create a system user."""
user = await self._store.async_create_user(
name=name,
system_generated=True,
is_active=True,
group_ids=[],
group_ids=group_ids or [],
)
self.hass.bus.async_fire(EVENT_USER_ADDED, {
@@ -188,6 +185,7 @@ class AuthManager:
credentials=credentials,
name=info.name,
is_active=info.is_active,
group_ids=[GROUP_ID_ADMIN],
)
self.hass.bus.async_fire(EVENT_USER_ADDED, {
@@ -217,6 +215,17 @@ class AuthManager:
'user_id': user.id
})
async def async_update_user(self, user: models.User,
name: Optional[str] = None,
group_ids: Optional[List[str]] = None) -> None:
"""Update a user."""
kwargs = {} # type: Dict[str,Any]
if name is not None:
kwargs['name'] = name
if group_ids is not None:
kwargs['group_ids'] = group_ids
await self._store.async_update_user(user, **kwargs)
async def async_activate_user(self, user: models.User) -> None:
"""Activate a user."""
await self._store.async_activate_user(user)

View File

@@ -1,4 +1,5 @@
"""Storage for auth models."""
import asyncio
from collections import OrderedDict
from datetime import timedelta
import hmac
@@ -11,7 +12,7 @@ from homeassistant.util import dt as dt_util
from . import models
from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
from .permissions import system_policies
from .permissions import PermissionLookup, system_policies
from .permissions.types import PolicyType # noqa: F401
STORAGE_VERSION = 1
@@ -34,6 +35,7 @@ class AuthStore:
self.hass = hass
self._users = None # type: Optional[Dict[str, models.User]]
self._groups = None # type: Optional[Dict[str, models.Group]]
self._perm_lookup = None # type: Optional[PermissionLookup]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True)
@@ -94,6 +96,7 @@ class AuthStore:
# Until we get group management, we just put everyone in the
# same group.
'groups': groups,
'perm_lookup': self._perm_lookup,
} # type: Dict[str, Any]
if is_owner is not None:
@@ -133,6 +136,33 @@ class AuthStore:
self._users.pop(user.id)
self._async_schedule_save()
async def async_update_user(
self, user: models.User, name: Optional[str] = None,
is_active: Optional[bool] = None,
group_ids: Optional[List[str]] = None) -> None:
"""Update a user."""
assert self._groups is not None
if group_ids is not None:
groups = []
for grid in group_ids:
group = self._groups.get(grid)
if group is None:
raise ValueError("Invalid group specified.")
groups.append(group)
user.groups = groups
user.invalidate_permission_cache()
for attr_name, value in (
('name', name),
('is_active', is_active),
):
if value is not None:
setattr(user, attr_name, value)
self._async_schedule_save()
async def async_activate_user(self, user: models.User) -> None:
"""Activate a user."""
user.is_active = True
@@ -242,13 +272,18 @@ class AuthStore:
async def _async_load(self) -> None:
"""Load the users."""
data = await self._store.async_load()
[ent_reg, data] = await asyncio.gather(
self.hass.helpers.entity_registry.async_get_registry(),
self._store.async_load(),
)
# Make sure that we're not overriding data if 2 loads happened at the
# same time
if self._users is not None:
return
self._perm_lookup = perm_lookup = PermissionLookup(ent_reg)
if data is None:
self._set_defaults()
return
@@ -347,6 +382,7 @@ class AuthStore:
is_owner=user_dict['is_owner'],
is_active=user_dict['is_active'],
system_generated=user_dict['system_generated'],
perm_lookup=perm_lookup,
)
for cred_dict in data['credentials']:
@@ -435,10 +471,11 @@ class AuthStore:
for group in self._groups.values():
g_dict = {
'id': group.id,
# Name not read for sys groups. Kept here for backwards compat
'name': group.name
} # type: Dict[str, Any]
if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN):
g_dict['name'] = group.name
g_dict['policy'] = group.policy
groups.append(g_dict)

View File

@@ -4,13 +4,14 @@ Sending HOTP through notify service
"""
import logging
from collections import OrderedDict
from typing import Any, Dict, Optional, Tuple, List # noqa: F401
from typing import Any, Dict, Optional, List
import attr
import voluptuous as vol
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers import config_validation as cv
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
@@ -314,8 +315,11 @@ class NotifySetupFlow(SetupFlow):
_generate_otp, self._secret, self._count)
assert self._notify_service
await self._auth_module.async_notify(
code, self._notify_service, self._target)
try:
await self._auth_module.async_notify(
code, self._notify_service, self._target)
except ServiceNotFound:
return self.async_abort(reason='notify_service_not_exist')
return self.async_show_form(
step_id='setup',

View File

@@ -8,6 +8,7 @@ import attr
from homeassistant.util import dt as dt_util
from . import permissions as perm_mdl
from .const import GROUP_ID_ADMIN
from .util import generate_secret
TOKEN_TYPE_NORMAL = 'normal'
@@ -30,6 +31,9 @@ class User:
"""A user."""
name = attr.ib(type=str) # type: Optional[str]
perm_lookup = attr.ib(
type=perm_mdl.PermissionLookup, cmp=False,
) # type: perm_mdl.PermissionLookup
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False)
@@ -48,7 +52,7 @@ class User:
) # type: Dict[str, RefreshToken]
_permissions = attr.ib(
type=perm_mdl.PolicyPermissions,
type=Optional[perm_mdl.PolicyPermissions],
init=False,
cmp=False,
default=None,
@@ -65,10 +69,24 @@ class User:
self._permissions = perm_mdl.PolicyPermissions(
perm_mdl.merge_policies([
group.policy for group in self.groups]))
group.policy for group in self.groups]),
self.perm_lookup)
return self._permissions
@property
def is_admin(self) -> bool:
"""Return if user is part of the admin group."""
if self.is_owner:
return True
return self.is_active and any(
gr.id == GROUP_ID_ADMIN for gr in self.groups)
def invalidate_permission_cache(self) -> None:
"""Invalidate permission cache."""
self._permissions = None
@attr.s(slots=True)
class RefreshToken:

View File

@@ -1,17 +1,18 @@
"""Permissions for Home Assistant."""
import logging
from typing import ( # noqa: F401
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union,
TYPE_CHECKING)
import voluptuous as vol
from homeassistant.core import State
from .const import CAT_ENTITIES
from .types import CategoryType, PolicyType
from .models import PermissionLookup
from .types import PolicyType
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
from .merge import merge_policies # noqa
POLICY_SCHEMA = vol.Schema({
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
})
@@ -22,49 +23,35 @@ _LOGGER = logging.getLogger(__name__)
class AbstractPermissions:
"""Default permissions class."""
def check_entity(self, entity_id: str, key: str) -> bool:
"""Test if we can access entity."""
_cached_entity_func = None
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
raise NotImplementedError
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
raise NotImplementedError
def check_entity(self, entity_id: str, key: str) -> bool:
"""Check if we can access entity."""
entity_func = self._cached_entity_func
if entity_func is None:
entity_func = self._cached_entity_func = self._entity_func()
return entity_func(entity_id, key)
class PolicyPermissions(AbstractPermissions):
"""Handle permissions."""
def __init__(self, policy: PolicyType) -> None:
def __init__(self, policy: PolicyType,
perm_lookup: PermissionLookup) -> None:
"""Initialize the permission class."""
self._policy = policy
self._compiled = {} # type: Dict[str, Callable[..., bool]]
self._perm_lookup = perm_lookup
def check_entity(self, entity_id: str, key: str) -> bool:
"""Test if we can access entity."""
func = self._policy_func(CAT_ENTITIES, compile_entities)
return func(entity_id, (key,))
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
func = self._policy_func(CAT_ENTITIES, compile_entities)
keys = ('read',)
return [entity for entity in states if func(entity.entity_id, keys)]
def _policy_func(self, category: str,
compile_func: Callable[[CategoryType], Callable]) \
-> Callable[..., bool]:
"""Get a policy function."""
func = self._compiled.get(category)
if func:
return func
func = self._compiled[category] = compile_func(
self._policy.get(category))
_LOGGER.debug("Compiled %s func: %s", category, func)
return func
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
return compile_entities(self._policy.get(CAT_ENTITIES),
self._perm_lookup)
def __eq__(self, other: Any) -> bool:
"""Equals check."""
@@ -78,13 +65,9 @@ class _OwnerPermissions(AbstractPermissions):
# pylint: disable=no-self-use
def check_entity(self, entity_id: str, key: str) -> bool:
"""Test if we can access entity."""
return True
def filter_states(self, states: List[State]) -> List[State]:
"""Filter a list of states for what the user is allowed to see."""
return states
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
return lambda entity_id, key: True
OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name

View File

@@ -1,11 +1,11 @@
"""Entity permissions."""
from functools import wraps
from typing import ( # noqa: F401
Callable, Dict, List, Tuple, Union)
from typing import Callable, List, Union # noqa: F401
import voluptuous as vol
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
from .models import PermissionLookup
from .types import CategoryType, ValueType
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
@@ -15,6 +15,7 @@ SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
}))
ENTITY_DOMAINS = 'domains'
ENTITY_DEVICE_IDS = 'device_ids'
ENTITY_ENTITY_IDS = 'entity_ids'
ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({
@@ -23,33 +24,34 @@ ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA,
vol.Optional(ENTITY_DEVICE_IDS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA,
}))
def _entity_allowed(schema: ValueType, keys: Tuple[str]) \
def _entity_allowed(schema: ValueType, key: str) \
-> Union[bool, None]:
"""Test if an entity is allowed based on the keys."""
if schema is None or isinstance(schema, bool):
return schema
assert isinstance(schema, dict)
return schema.get(keys[0])
return schema.get(key)
def compile_entities(policy: CategoryType) \
-> Callable[[str, Tuple[str]], bool]:
def compile_entities(policy: CategoryType, perm_lookup: PermissionLookup) \
-> Callable[[str, str], bool]:
"""Compile policy into a function that tests policy."""
# None, Empty Dict, False
if not policy:
def apply_policy_deny_all(entity_id: str, keys: Tuple[str]) -> bool:
def apply_policy_deny_all(entity_id: str, key: str) -> bool:
"""Decline all."""
return False
return apply_policy_deny_all
if policy is True:
def apply_policy_allow_all(entity_id: str, keys: Tuple[str]) -> bool:
def apply_policy_allow_all(entity_id: str, key: str) -> bool:
"""Approve all."""
return True
@@ -58,10 +60,11 @@ def compile_entities(policy: CategoryType) \
assert isinstance(policy, dict)
domains = policy.get(ENTITY_DOMAINS)
device_ids = policy.get(ENTITY_DEVICE_IDS)
entity_ids = policy.get(ENTITY_ENTITY_IDS)
all_entities = policy.get(SUBCAT_ALL)
funcs = [] # type: List[Callable[[str, Tuple[str]], Union[None, bool]]]
funcs = [] # type: List[Callable[[str, str], Union[None, bool]]]
# The order of these functions matter. The more precise are at the top.
# If a function returns None, they cannot handle it.
@@ -70,23 +73,46 @@ def compile_entities(policy: CategoryType) \
# Setting entity_ids to a boolean is final decision for permissions
# So return right away.
if isinstance(entity_ids, bool):
def allowed_entity_id_bool(entity_id: str, keys: Tuple[str]) -> bool:
def allowed_entity_id_bool(entity_id: str, key: str) -> bool:
"""Test if allowed entity_id."""
return entity_ids # type: ignore
return allowed_entity_id_bool
if entity_ids is not None:
def allowed_entity_id_dict(entity_id: str, keys: Tuple[str]) \
def allowed_entity_id_dict(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed entity_id."""
return _entity_allowed(
entity_ids.get(entity_id), keys) # type: ignore
entity_ids.get(entity_id), key) # type: ignore
funcs.append(allowed_entity_id_dict)
if isinstance(device_ids, bool):
def allowed_device_id_bool(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed device_id."""
return device_ids
funcs.append(allowed_device_id_bool)
elif device_ids is not None:
def allowed_device_id_dict(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed device_id."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
if entity_entry is None or entity_entry.device_id is None:
return None
return _entity_allowed(
device_ids.get(entity_entry.device_id), key # type: ignore
)
funcs.append(allowed_device_id_dict)
if isinstance(domains, bool):
def allowed_domain_bool(entity_id: str, keys: Tuple[str]) \
def allowed_domain_bool(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed domain."""
return domains
@@ -94,31 +120,31 @@ def compile_entities(policy: CategoryType) \
funcs.append(allowed_domain_bool)
elif domains is not None:
def allowed_domain_dict(entity_id: str, keys: Tuple[str]) \
def allowed_domain_dict(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed domain."""
domain = entity_id.split(".", 1)[0]
return _entity_allowed(domains.get(domain), keys) # type: ignore
return _entity_allowed(domains.get(domain), key) # type: ignore
funcs.append(allowed_domain_dict)
if isinstance(all_entities, bool):
def allowed_all_entities_bool(entity_id: str, keys: Tuple[str]) \
def allowed_all_entities_bool(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed domain."""
return all_entities
funcs.append(allowed_all_entities_bool)
elif all_entities is not None:
def allowed_all_entities_dict(entity_id: str, keys: Tuple[str]) \
def allowed_all_entities_dict(entity_id: str, key: str) \
-> Union[None, bool]:
"""Test if allowed domain."""
return _entity_allowed(all_entities, keys)
return _entity_allowed(all_entities, key)
funcs.append(allowed_all_entities_dict)
# Can happen if no valid subcategories specified
if not funcs:
def apply_policy_deny_all_2(entity_id: str, keys: Tuple[str]) -> bool:
def apply_policy_deny_all_2(entity_id: str, key: str) -> bool:
"""Decline all."""
return False
@@ -128,16 +154,16 @@ def compile_entities(policy: CategoryType) \
func = funcs[0]
@wraps(func)
def apply_policy_func(entity_id: str, keys: Tuple[str]) -> bool:
def apply_policy_func(entity_id: str, key: str) -> bool:
"""Apply a single policy function."""
return func(entity_id, keys) is True
return func(entity_id, key) is True
return apply_policy_func
def apply_policy_funcs(entity_id: str, keys: Tuple[str]) -> bool:
def apply_policy_funcs(entity_id: str, key: str) -> bool:
"""Apply several policy functions."""
for func in funcs:
result = func(entity_id, keys)
result = func(entity_id, key)
if result is not None:
return result
return False

View File

@@ -0,0 +1,17 @@
"""Models for permissions."""
from typing import TYPE_CHECKING
import attr
if TYPE_CHECKING:
# pylint: disable=unused-import
from homeassistant.helpers import ( # noqa
entity_registry as ent_reg,
)
@attr.s(slots=True)
class PermissionLookup:
"""Class to hold data for permission lookups."""
entity_registry = attr.ib(type='ent_reg.EntityRegistry')

View File

@@ -1,6 +1,5 @@
"""Common code for permissions."""
from typing import ( # noqa: F401
Mapping, Union, Any)
from typing import Mapping, Union
# MyPy doesn't support recursion yet. So writing it out as far as we need.

View File

@@ -226,7 +226,11 @@ class LoginFlow(data_entry_flow.FlowHandler):
if user_input is None and hasattr(auth_module,
'async_initialize_login_mfa_step'):
await auth_module.async_initialize_login_mfa_step(self.user.id)
try:
await auth_module.async_initialize_login_mfa_step(self.user.id)
except HomeAssistantError:
_LOGGER.exception('Error initializing MFA step')
return self.async_abort(reason='unknown_error')
if user_input is not None:
expires = self.created_at + MFA_SESSION_EXPIRATION

View File

@@ -1,8 +1,6 @@
"""Home Assistant auth provider."""
import base64
from collections import OrderedDict
import hashlib
import hmac
from typing import Any, Dict, List, Optional, cast
import bcrypt
@@ -11,12 +9,10 @@ import voluptuous as vol
from homeassistant.const import CONF_ID
from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.async_ import run_coroutine_threadsafe
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
from ..util import generate_secret
STORAGE_VERSION = 1
@@ -62,7 +58,6 @@ class Data:
if data is None:
data = {
'salt': generate_secret(),
'users': []
}
@@ -94,39 +89,11 @@ class Data:
user_hash = base64.b64decode(found['password'])
# if the hash is not a bcrypt hash...
# provide a transparant upgrade for old pbkdf2 hash format
if not (user_hash.startswith(b'$2a$')
or user_hash.startswith(b'$2b$')
or user_hash.startswith(b'$2x$')
or user_hash.startswith(b'$2y$')):
# IMPORTANT! validate the login, bail if invalid
hashed = self.legacy_hash_password(password)
if not hmac.compare_digest(hashed, user_hash):
raise InvalidAuth
# then re-hash the valid password with bcrypt
self.change_password(found['username'], password)
run_coroutine_threadsafe(
self.async_save(), self.hass.loop
).result()
user_hash = base64.b64decode(found['password'])
# bcrypt.checkpw is timing-safe
if not bcrypt.checkpw(password.encode(),
user_hash):
raise InvalidAuth
def legacy_hash_password(self, password: str,
for_storage: bool = False) -> bytes:
"""LEGACY password encoding."""
# We're no longer storing salts in data, but if one exists we
# should be able to retrieve it.
salt = self._data['salt'].encode() # type: ignore
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
if for_storage:
hashed = base64.b64encode(hashed)
return hashed
# pylint: disable=no-self-use
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password."""

View File

@@ -4,16 +4,19 @@ Support Legacy API password auth provider.
It will be removed when auth system production ready
"""
import hmac
from typing import Any, Dict, Optional, cast
from typing import Any, Dict, Optional, cast, TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
from .. import AuthManager
from ..models import Credentials, UserMeta, User
if TYPE_CHECKING:
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
USER_SCHEMA = vol.Schema({
@@ -31,6 +34,24 @@ class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
async def async_get_user(hass: HomeAssistant) -> User:
"""Return the legacy API password user."""
auth = cast(AuthManager, hass.auth) # type: ignore
found = None
for prv in auth.auth_providers:
if prv.type == 'legacy_api_password':
found = prv
break
if found is None:
raise ValueError('Legacy API password provider not found')
return await auth.async_get_or_create_user(
await found.async_get_or_create_credentials({})
)
@AUTH_PROVIDERS.register('legacy_api_password')
class LegacyApiPasswordAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""

View File

@@ -115,11 +115,6 @@ async def async_from_config_dict(config: Dict[str, Any],
conf_util.merge_packages_config(
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
# Ensure we have no None values after merge
for key, value in config.items():
if not value:
config[key] = {}
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_load()

View File

@@ -25,21 +25,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return
data = hass.data[BLINK_DATA]
# Current version of blinkpy API only supports one sync module. When
# support for additional models is added, the sync module name should
# come from the API.
sync_modules = []
sync_modules.append(BlinkSyncModule(data, 'sync'))
for sync_name, sync_module in data.sync.items():
sync_modules.append(BlinkSyncModule(data, sync_name, sync_module))
add_entities(sync_modules, True)
class BlinkSyncModule(AlarmControlPanel):
"""Representation of a Blink Alarm Control Panel."""
def __init__(self, data, name):
def __init__(self, data, name, sync):
"""Initialize the alarm control panel."""
self.data = data
self.sync = data.sync
self.sync = sync
self._name = name
self._state = None
@@ -68,6 +66,7 @@ class BlinkSyncModule(AlarmControlPanel):
"""Return the state attributes."""
attr = self.sync.attributes
attr['network_info'] = self.data.networks
attr['associated_cameras'] = list(self.sync.cameras.keys())
attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
return attr

View File

@@ -12,7 +12,8 @@ from homeassistant.components.lupusec import DOMAIN as LUPUSEC_DOMAIN
from homeassistant.components.lupusec import LupusecDevice
from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED)
DEPENDENCIES = ['lupusec']
@@ -50,6 +51,8 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanel):
state = STATE_ALARM_ARMED_AWAY
elif self._device.is_home:
state = STATE_ALARM_ARMED_HOME
elif self._device.is_alarm_triggered:
state = STATE_ALARM_TRIGGERED
else:
state = None
return state

View File

@@ -21,7 +21,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time
import homeassistant.util.dt as dt_util
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.restore_state import RestoreEntity
_LOGGER = logging.getLogger(__name__)
@@ -116,7 +116,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
)])
class ManualAlarm(alarm.AlarmControlPanel):
class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity):
"""
Representation of an alarm status.
@@ -310,7 +310,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
async def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
state = await async_get_last_state(self.hass, self.entity_id)
state = await self.async_get_last_state()
if state:
self._state = state.state
self._state_ts = state.last_updated

View File

@@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate)
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate, subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -51,7 +51,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None):
"""Set up MQTT alarm control panel through configuration.yaml."""
await _async_setup_entity(hass, config, async_add_entities)
await _async_setup_entity(config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -59,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_discover(discovery_payload):
"""Discover and add an MQTT alarm control panel."""
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(hass, config, async_add_entities,
await _async_setup_entity(config, async_add_entities,
discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect(
@@ -67,54 +67,47 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_discover)
async def _async_setup_entity(hass, config, async_add_entities,
async def _async_setup_entity(config, async_add_entities,
discovery_hash=None):
"""Set up the MQTT Alarm Control Panel platform."""
async_add_entities([MqttAlarm(
config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC),
config.get(CONF_QOS),
config.get(CONF_RETAIN),
config.get(CONF_PAYLOAD_DISARM),
config.get(CONF_PAYLOAD_ARM_HOME),
config.get(CONF_PAYLOAD_ARM_AWAY),
config.get(CONF_CODE),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
discovery_hash,)])
async_add_entities([MqttAlarm(config, discovery_hash)])
class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, retain,
payload_disarm, payload_arm_home, payload_arm_away, code,
availability_topic, payload_available, payload_not_available,
discovery_hash):
def __init__(self, config, discovery_hash):
"""Init the MQTT Alarm Control Panel."""
self._state = STATE_UNKNOWN
self._config = config
self._sub_state = None
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS)
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
self._state = STATE_UNKNOWN
self._name = name
self._state_topic = state_topic
self._command_topic = command_topic
self._qos = qos
self._retain = retain
self._payload_disarm = payload_disarm
self._payload_arm_home = payload_arm_home
self._payload_arm_away = payload_arm_away
self._code = code
self._discovery_hash = discovery_hash
MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update)
async def async_added_to_hass(self):
"""Subscribe mqtt events."""
await MqttAvailability.async_added_to_hass(self)
await MqttDiscoveryUpdate.async_added_to_hass(self)
await super().async_added_to_hass()
await self._subscribe_topics()
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
await self.availability_discovery_update(config)
await self._subscribe_topics()
self.async_schedule_update_ha_state()
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
def message_received(topic, payload, qos):
"""Run when new MQTT message has been received."""
@@ -126,8 +119,16 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
self._state = payload
self.async_schedule_update_ha_state()
await mqtt.async_subscribe(
self.hass, self._state_topic, message_received, self._qos)
self._sub_state = await subscription.async_subscribe_topics(
self.hass, self._sub_state,
{'state_topic': {'topic': self._config.get(CONF_STATE_TOPIC),
'msg_callback': message_received,
'qos': self._config.get(CONF_QOS)}})
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
await MqttAvailability.async_will_remove_from_hass(self)
@property
def should_poll(self):
@@ -137,7 +138,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
@property
def name(self):
"""Return the name of the device."""
return self._name
return self._config.get(CONF_NAME)
@property
def state(self):
@@ -147,9 +148,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
@property
def code_format(self):
"""Return one or more digits/characters."""
if self._code is None:
code = self._config.get(CONF_CODE)
if code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
if isinstance(code, str) and re.search('^\\d+$', code):
return 'Number'
return 'Any'
@@ -161,8 +163,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
if not self._validate_code(code, 'disarming'):
return
mqtt.async_publish(
self.hass, self._command_topic, self._payload_disarm, self._qos,
self._retain)
self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._config.get(CONF_PAYLOAD_DISARM),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
async def async_alarm_arm_home(self, code=None):
"""Send arm home command.
@@ -172,8 +176,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
if not self._validate_code(code, 'arming home'):
return
mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_home, self._qos,
self._retain)
self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._config.get(CONF_PAYLOAD_ARM_HOME),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
async def async_alarm_arm_away(self, code=None):
"""Send arm away command.
@@ -183,12 +189,15 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
if not self._validate_code(code, 'arming away'):
return
mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_away, self._qos,
self._retain)
self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._config.get(CONF_PAYLOAD_ARM_AWAY),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
def _validate_code(self, code, state):
"""Validate given code."""
check = self._code is None or code == self._code
conf_code = self._config.get(CONF_CODE)
check = conf_code is None or code == conf_code
if not check:
_LOGGER.warning('Wrong code entered for %s', state)
return check

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['yalesmartalarmclient==0.1.4']
REQUIREMENTS = ['yalesmartalarmclient==0.1.5']
CONF_AREA_ID = 'area_id'

View File

@@ -504,6 +504,20 @@ class _AlexaColorTemperatureController(_AlexaInterface):
def name(self):
return 'Alexa.ColorTemperatureController'
def properties_supported(self):
return [{'name': 'colorTemperatureInKelvin'}]
def properties_retrievable(self):
return True
def get_property(self, name):
if name != 'colorTemperatureInKelvin':
raise _UnsupportedProperty(name)
if 'color_temp' in self.entity.attributes:
return color_util.color_temperature_mired_to_kelvin(
self.entity.attributes['color_temp'])
return 0
class _AlexaPercentageController(_AlexaInterface):
"""Implements Alexa.PercentageController.

View File

@@ -9,7 +9,9 @@ import json
import logging
from aiohttp import web
from aiohttp.web_exceptions import HTTPBadRequest
import async_timeout
import voluptuous as vol
from homeassistant.bootstrap import DATA_LOGGING
from homeassistant.components.http import HomeAssistantView
@@ -20,7 +22,9 @@ from homeassistant.const import (
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
URL_API_TEMPLATE, __version__)
import homeassistant.core as ha
from homeassistant.exceptions import TemplateError
from homeassistant.auth.permissions.const import POLICY_READ
from homeassistant.exceptions import (
TemplateError, Unauthorized, ServiceNotFound)
from homeassistant.helpers import template
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
@@ -81,6 +85,8 @@ class APIEventStream(HomeAssistantView):
async def get(self, request):
"""Provide a streaming interface for the event bus."""
if not request['hass_user'].is_admin:
raise Unauthorized()
hass = request.app['hass']
stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop)
@@ -185,7 +191,13 @@ class APIStatesView(HomeAssistantView):
@ha.callback
def get(self, request):
"""Get current states."""
return self.json(request.app['hass'].states.async_all())
user = request['hass_user']
entity_perm = user.permissions.check_entity
states = [
state for state in request.app['hass'].states.async_all()
if entity_perm(state.entity_id, 'read')
]
return self.json(states)
class APIEntityStateView(HomeAssistantView):
@@ -197,6 +209,10 @@ class APIEntityStateView(HomeAssistantView):
@ha.callback
def get(self, request, entity_id):
"""Retrieve state of entity."""
user = request['hass_user']
if not user.permissions.check_entity(entity_id, POLICY_READ):
raise Unauthorized(entity_id=entity_id)
state = request.app['hass'].states.get(entity_id)
if state:
return self.json(state)
@@ -204,6 +220,8 @@ class APIEntityStateView(HomeAssistantView):
async def post(self, request, entity_id):
"""Update state of entity."""
if not request['hass_user'].is_admin:
raise Unauthorized(entity_id=entity_id)
hass = request.app['hass']
try:
data = await request.json()
@@ -236,6 +254,8 @@ class APIEntityStateView(HomeAssistantView):
@ha.callback
def delete(self, request, entity_id):
"""Remove entity."""
if not request['hass_user'].is_admin:
raise Unauthorized(entity_id=entity_id)
if request.app['hass'].states.async_remove(entity_id):
return self.json_message("Entity removed.")
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
@@ -261,6 +281,8 @@ class APIEventView(HomeAssistantView):
async def post(self, request, event_type):
"""Fire events."""
if not request['hass_user'].is_admin:
raise Unauthorized()
body = await request.text()
try:
event_data = json.loads(body) if body else None
@@ -320,8 +342,11 @@ class APIDomainServicesView(HomeAssistantView):
"Data should be valid JSON.", HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states:
await hass.services.async_call(
domain, service, data, True, self.context(request))
try:
await hass.services.async_call(
domain, service, data, True, self.context(request))
except (vol.Invalid, ServiceNotFound):
raise HTTPBadRequest()
return self.json(changed_states)
@@ -346,6 +371,8 @@ class APITemplateView(HomeAssistantView):
async def post(self, request):
"""Render a template."""
if not request['hass_user'].is_admin:
raise Unauthorized()
try:
data = await request.json()
tpl = template.Template(data['template'], request.app['hass'])
@@ -363,6 +390,8 @@ class APIErrorLog(HomeAssistantView):
async def get(self, request):
"""Retrieve API error log."""
if not request['hass_user'].is_admin:
raise Unauthorized()
return web.FileResponse(request.app['hass'].data[DATA_LOGGING])

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.10']
REQUIREMENTS = ['pyatv==0.3.12']
_LOGGER = logging.getLogger(__name__)

View File

@@ -14,7 +14,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
REQUIREMENTS = ['aioasuswrt==1.1.11']
REQUIREMENTS = ['aioasuswrt==1.1.13']
_LOGGER = logging.getLogger(__name__)

View File

@@ -11,7 +11,6 @@ import voluptuous as vol
from requests import RequestException
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery
@@ -141,11 +140,11 @@ def setup(hass, config):
from requests import Session
conf = config[DOMAIN]
api_http_session = None
try:
api_http_session = Session()
except RequestException as ex:
_LOGGER.warning("Creating HTTP session failed with: %s", str(ex))
api_http_session = None
api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)
@@ -157,6 +156,20 @@ def setup(hass, config):
install_id=conf.get(CONF_INSTALL_ID),
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE))
def close_http_session(event):
"""Close API sessions used to connect to August."""
_LOGGER.debug("Closing August HTTP sessions")
if api_http_session:
try:
api_http_session.close()
except RequestException:
pass
_LOGGER.debug("August HTTP session closed.")
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
_LOGGER.debug("Registered for HASS stop event")
return setup_august(hass, config, api, authenticator)
@@ -178,22 +191,6 @@ class AugustData:
self._door_state_by_id = {}
self._activities_by_id = {}
@callback
def august_api_stop(event):
"""Close the API HTTP session."""
_LOGGER.debug("Closing August HTTP session")
try:
self._api.http_session.close()
self._api.http_session = None
except RequestException:
pass
_LOGGER.debug("August HTTP session closed.")
self._hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, august_api_stop)
_LOGGER.debug("Registered for HASS stop event")
@property
def house_ids(self):
"""Return a list of house_ids."""

View File

@@ -13,7 +13,7 @@
"title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
},
"setup": {
"description": "**notify.{notify_service}** ha enviat una contrasenya d'un sol \u00fas. Introdu\u00efu-la a continuaci\u00f3:",
"description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdu\u00efu-la a continuaci\u00f3:",
"title": "Verifiqueu la configuraci\u00f3"
}
},

View File

@@ -13,6 +13,7 @@
"title": "Nastavte jednor\u00e1zov\u00e9 heslo dodan\u00e9 komponentou notify"
},
"setup": {
"description": "Jednor\u00e1zov\u00e9 heslo bylo odesl\u00e1no prost\u0159ednictv\u00edm **notify.{notify_service}**. Zadejte jej n\u00ed\u017ee:",
"title": "Ov\u011b\u0159en\u00ed nastaven\u00ed"
}
}
@@ -20,7 +21,14 @@
"totp": {
"error": {
"invalid_code": "Neplatn\u00fd k\u00f3d, zkuste to znovu. Pokud se tato chyba opakuje, ujist\u011bte se, \u017ee hodiny syst\u00e9mu Home Assistant jsou spr\u00e1vn\u011b nastaveny."
}
},
"step": {
"init": {
"description": "Chcete-li aktivovat dvoufaktorovou autentizaci pomoc\u00ed jednor\u00e1zov\u00fdch hesel zalo\u017een\u00fdch na \u010dase, na\u010dt\u011bte k\u00f3d QR pomoc\u00ed va\u0161\u00ed autentiza\u010dn\u00ed aplikace. Pokud ji nem\u00e1te, doporu\u010dujeme bu\u010f [Google Authenticator](https://support.google.com/accounts/answer/1066447) nebo [Authy](https://authy.com/). \n\n {qr_code} \n \n Po skenov\u00e1n\u00ed k\u00f3du zadejte \u0161estcifern\u00fd k\u00f3d z aplikace a ov\u011b\u0159te nastaven\u00ed. Pokud m\u00e1te probl\u00e9my se skenov\u00e1n\u00edm k\u00f3du QR, prove\u010fte ru\u010dn\u00ed nastaven\u00ed s k\u00f3dem **`{code}`**.",
"title": "Nastavte dvoufaktorovou autentizaci pomoc\u00ed TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@@ -2,18 +2,18 @@
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Ni na voljo storitev obve\u0161\u010danja."
"no_available_service": "Storitve obve\u0161\u010danja niso na voljo."
},
"error": {
"invalid_code": "Neveljavna koda, poskusite znova."
},
"step": {
"init": {
"description": "Prosimo, izberite eno od storitev obve\u0161\u010danja:",
"description": "Izberite eno od storitev obve\u0161\u010danja:",
"title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento"
},
"setup": {
"description": "Enkratno geslo je poslal **notify.{notify_service} **. Vnesite ga spodaj:",
"description": "Enkratno geslo je poslal **notify.{notify_service} **. Prosimo, vnesite ga spodaj:",
"title": "Preverite nastavitev"
}
},

View File

@@ -16,12 +16,13 @@ from homeassistant.core import CoreState
from homeassistant.loader import bind_hass
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID,
EVENT_AUTOMATION_TRIGGERED, ATTR_NAME)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv
@@ -182,7 +183,7 @@ async def async_setup(hass, config):
return True
class AutomationEntity(ToggleEntity):
class AutomationEntity(ToggleEntity, RestoreEntity):
"""Entity to show status of entity."""
def __init__(self, automation_id, name, async_attach_triggers, cond_func,
@@ -227,12 +228,13 @@ class AutomationEntity(ToggleEntity):
async def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state."""
await super().async_added_to_hass()
if self._initial_state is not None:
enable_automation = self._initial_state
_LOGGER.debug("Automation %s initial state %s from config "
"initial_state", self.entity_id, enable_automation)
else:
state = await async_get_last_state(self.hass, self.entity_id)
state = await self.async_get_last_state()
if state:
enable_automation = state.state == STATE_ON
self._last_triggered = state.attributes.get('last_triggered')
@@ -285,12 +287,17 @@ class AutomationEntity(ToggleEntity):
"""
if skip_condition or self._cond_func(variables):
self.async_set_context(context)
self.hass.bus.async_fire(EVENT_AUTOMATION_TRIGGERED, {
ATTR_NAME: self._name,
ATTR_ENTITY_ID: self.entity_id,
}, context=context)
await self._async_action(self.entity_id, variables, context)
self._last_triggered = utcnow()
await self.async_update_ha_state()
async def async_will_remove_from_hass(self):
"""Remove listeners when removing automation from HASS."""
await super().async_will_remove_from_hass()
await self.async_turn_off()
async def async_enable(self):
@@ -368,8 +375,6 @@ def _async_get_action(hass, config, name):
async def action(entity_id, variables, context):
"""Execute an action."""
_LOGGER.info('Executing %s', name)
hass.components.logbook.async_log_entry(
name, 'has been triggered', DOMAIN, entity_id)
await script_obj.async_run(variables, context)
return action

View File

@@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[BLINK_DATA]
devs = []
for camera in data.sync.cameras:
for camera in data.cameras:
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
devs.append(BlinkBinarySensor(data, camera, sensor_type))
add_entities(devs, True)
@@ -34,7 +34,7 @@ class BlinkBinarySensor(BinarySensorDevice):
name, icon = BINARY_SENSORS[sensor_type]
self._name = "{} {} {}".format(BLINK_DATA, camera, name)
self._icon = icon
self._camera = data.sync.cameras[camera]
self._camera = data.cameras[camera]
self._state = None
self._unique_id = "{}-{}".format(self._camera.serial, self._type)

View File

@@ -16,6 +16,8 @@ DEPENDENCIES = ['fibaro']
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'com.fibaro.floodSensor': ['Flood', 'mdi:water', 'flood'],
'com.fibaro.motionSensor': ['Motion', 'mdi:run', 'motion'],
'com.fibaro.doorSensor': ['Door', 'mdi:window-open', 'door'],
'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'],
'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'],

View File

@@ -3,59 +3,39 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ihc/
"""
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
BinarySensorDevice)
from homeassistant.components.ihc import (
validate_name, IHC_DATA, IHC_CONTROLLER, IHC_INFO)
from homeassistant.components.ihc.const import CONF_INVERTING
IHC_DATA, IHC_CONTROLLER, IHC_INFO)
from homeassistant.components.ihc.const import (
CONF_INVERTING)
from homeassistant.components.ihc.ihcdevice import IHCDevice
from homeassistant.const import (
CONF_NAME, CONF_TYPE, CONF_ID, CONF_BINARY_SENSORS)
import homeassistant.helpers.config_validation as cv
CONF_TYPE)
DEPENDENCIES = ['ihc']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_BINARY_SENSORS, default=[]):
vol.All(cv.ensure_list, [
vol.All({
vol.Required(CONF_ID): cv.positive_int,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_INVERTING, default=False): cv.boolean,
}, validate_name)
])
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the IHC binary sensor platform."""
ihc_controller = hass.data[IHC_DATA][IHC_CONTROLLER]
info = hass.data[IHC_DATA][IHC_INFO]
if discovery_info is None:
return
devices = []
if discovery_info:
for name, device in discovery_info.items():
ihc_id = device['ihc_id']
product_cfg = device['product_cfg']
product = device['product']
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
product_cfg.get(CONF_TYPE),
product_cfg[CONF_INVERTING],
product)
devices.append(sensor)
else:
binary_sensors = config[CONF_BINARY_SENSORS]
for sensor_cfg in binary_sensors:
ihc_id = sensor_cfg[CONF_ID]
name = sensor_cfg[CONF_NAME]
sensor_type = sensor_cfg.get(CONF_TYPE)
inverting = sensor_cfg[CONF_INVERTING]
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
sensor_type, inverting)
devices.append(sensor)
for name, device in discovery_info.items():
ihc_id = device['ihc_id']
product_cfg = device['product_cfg']
product = device['product']
# Find controller that corresponds with device id
ctrl_id = device['ctrl_id']
ihc_key = IHC_DATA.format(ctrl_id)
info = hass.data[ihc_key][IHC_INFO]
ihc_controller = hass.data[ihc_key][IHC_CONTROLLER]
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
product_cfg.get(CONF_TYPE),
product_cfg[CONF_INVERTING],
product)
devices.append(sensor)
add_entities(devices)

View File

@@ -45,8 +45,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_OFF_DELAY):
vol.All(vol.Coerce(int), vol.Range(min=0)),
# Integrations shouldn't never expose unique_id through configuration
# this here is an exception because MQTT is a msg transport, not a protocol
# Integrations should never expose unique_id through configuration.
# This is an exception because MQTT is a message transport, not a protocol
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@@ -55,7 +55,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None):
"""Set up MQTT binary sensor through configuration.yaml."""
await _async_setup_entity(hass, config, async_add_entities)
await _async_setup_entity(config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -63,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_discover(discovery_payload):
"""Discover and add a MQTT binary sensor."""
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(hass, config, async_add_entities,
await _async_setup_entity(config, async_add_entities,
discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect(
@@ -71,17 +71,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_discover)
async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None):
async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
"""Set up the MQTT binary sensor."""
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = hass
async_add_entities([MqttBinarySensor(
config,
discovery_hash
)])
async_add_entities([MqttBinarySensor(config, discovery_hash)])
class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
@@ -91,30 +83,18 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
def __init__(self, config, discovery_hash):
"""Initialize the MQTT binary sensor."""
self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = None
self._sub_state = None
self._delay_listener = None
self._name = None
self._state_topic = None
self._device_class = None
self._payload_on = None
self._payload_off = None
self._qos = None
self._force_update = None
self._off_delay = None
self._template = None
self._unique_id = None
# Load config
self._setup_from_config(config)
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE)
MqttAvailability.__init__(self, availability_topic, self._qos,
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update)
@@ -122,37 +102,23 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
async def async_added_to_hass(self):
"""Subscribe mqtt events."""
await MqttAvailability.async_added_to_hass(self)
await MqttDiscoveryUpdate.async_added_to_hass(self)
await super().async_added_to_hass()
await self._subscribe_topics()
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._setup_from_config(config)
self._config = config
await self.availability_discovery_update(config)
await self._subscribe_topics()
self.async_schedule_update_ha_state()
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
self._name = config.get(CONF_NAME)
self._state_topic = config.get(CONF_STATE_TOPIC)
self._device_class = config.get(CONF_DEVICE_CLASS)
self._qos = config.get(CONF_QOS)
self._force_update = config.get(CONF_FORCE_UPDATE)
self._off_delay = config.get(CONF_OFF_DELAY)
self._payload_on = config.get(CONF_PAYLOAD_ON)
self._payload_off = config.get(CONF_PAYLOAD_OFF)
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None and value_template.hass is None:
value_template.hass = self.hass
self._template = value_template
self._unique_id = config.get(CONF_UNIQUE_ID)
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
@callback
def off_delay_listener(now):
"""Switch device off after a delay."""
@@ -163,34 +129,37 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
@callback
def state_message_received(_topic, payload, _qos):
"""Handle a new received MQTT state message."""
if self._template is not None:
payload = self._template.async_render_with_possible_json_value(
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
payload = value_template.async_render_with_possible_json_value(
payload)
if payload == self._payload_on:
if payload == self._config.get(CONF_PAYLOAD_ON):
self._state = True
elif payload == self._payload_off:
elif payload == self._config.get(CONF_PAYLOAD_OFF):
self._state = False
else: # Payload is not for this entity
_LOGGER.warning('No matching payload found'
' for entity: %s with state_topic: %s',
self._name, self._state_topic)
self._config.get(CONF_NAME),
self._config.get(CONF_STATE_TOPIC))
return
if self._delay_listener is not None:
self._delay_listener()
self._delay_listener = None
if (self._state and self._off_delay is not None):
off_delay = self._config.get(CONF_OFF_DELAY)
if (self._state and off_delay is not None):
self._delay_listener = evt.async_call_later(
self.hass, self._off_delay, off_delay_listener)
self.hass, off_delay, off_delay_listener)
self.async_schedule_update_ha_state()
self._sub_state = await subscription.async_subscribe_topics(
self.hass, self._sub_state,
{'state_topic': {'topic': self._state_topic,
{'state_topic': {'topic': self._config.get(CONF_STATE_TOPIC),
'msg_callback': state_message_received,
'qos': self._qos}})
'qos': self._config.get(CONF_QOS)}})
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
@@ -205,7 +174,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
@property
def name(self):
"""Return the name of the binary sensor."""
return self._name
return self._config.get(CONF_NAME)
@property
def is_on(self):
@@ -215,12 +184,12 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
return self._config.get(CONF_DEVICE_CLASS)
@property
def force_update(self):
"""Force update."""
return self._force_update
return self._config.get(CONF_FORCE_UPDATE)
@property
def unique_id(self):

View File

@@ -7,9 +7,11 @@ https://home-assistant.io/components/binary_sensor.point/
import logging
from homeassistant.components.binary_sensor import (
DOMAIN as PARENT_DOMAIN, BinarySensorDevice)
from homeassistant.components.point import MinutPointEntity
from homeassistant.components.point.const import (
DOMAIN as POINT_DOMAIN, NEW_DEVICE, SIGNAL_WEBHOOK)
DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -39,13 +41,19 @@ EVENTS = {
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Point's binary sensors based on a config entry."""
device_id = config_entry.data[NEW_DEVICE]
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
async_add_entities((MinutPointBinarySensor(client, device_id, device_class)
for device_class in EVENTS), True)
async def async_discover_sensor(device_id):
"""Discover and add a discovered sensor."""
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
async_add_entities(
(MinutPointBinarySensor(client, device_id, device_class)
for device_class in EVENTS), True)
async_dispatcher_connect(
hass, POINT_DISCOVERY_NEW.format(PARENT_DOMAIN, POINT_DOMAIN),
async_discover_sensor)
class MinutPointBinarySensor(MinutPointEntity):
class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice):
"""The platform class required by Home Assistant."""
def __init__(self, point_client, device_id, device_class):

View File

@@ -74,7 +74,7 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update(self):
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)

View File

@@ -14,46 +14,48 @@ DEPENDENCIES = ['sense']
_LOGGER = logging.getLogger(__name__)
BIN_SENSOR_CLASS = 'power'
MDI_ICONS = {'ac': 'air-conditioner',
'aquarium': 'fish',
'car': 'car-electric',
'computer': 'desktop-classic',
'cup': 'coffee',
'dehumidifier': 'water-off',
'dishes': 'dishwasher',
'drill': 'toolbox',
'fan': 'fan',
'freezer': 'fridge-top',
'fridge': 'fridge-bottom',
'game': 'gamepad-variant',
'garage': 'garage',
'grill': 'stove',
'heat': 'fire',
'heater': 'radiatior',
'humidifier': 'water',
'kettle': 'kettle',
'leafblower': 'leaf',
'lightbulb': 'lightbulb',
'media_console': 'set-top-box',
'modem': 'router-wireless',
'outlet': 'power-socket-us',
'papershredder': 'shredder',
'printer': 'printer',
'pump': 'water-pump',
'settings': 'settings',
'skillet': 'pot',
'smartcamera': 'webcam',
'socket': 'power-plug',
'sound': 'speaker',
'stove': 'stove',
'trash': 'trash-can',
'tv': 'television',
'vacuum': 'robot-vacuum',
'washer': 'washing-machine'}
MDI_ICONS = {
'ac': 'air-conditioner',
'aquarium': 'fish',
'car': 'car-electric',
'computer': 'desktop-classic',
'cup': 'coffee',
'dehumidifier': 'water-off',
'dishes': 'dishwasher',
'drill': 'toolbox',
'fan': 'fan',
'freezer': 'fridge-top',
'fridge': 'fridge-bottom',
'game': 'gamepad-variant',
'garage': 'garage',
'grill': 'stove',
'heat': 'fire',
'heater': 'radiatior',
'humidifier': 'water',
'kettle': 'kettle',
'leafblower': 'leaf',
'lightbulb': 'lightbulb',
'media_console': 'set-top-box',
'modem': 'router-wireless',
'outlet': 'power-socket-us',
'papershredder': 'shredder',
'printer': 'printer',
'pump': 'water-pump',
'settings': 'settings',
'skillet': 'pot',
'smartcamera': 'webcam',
'socket': 'power-plug',
'sound': 'speaker',
'stove': 'stove',
'trash': 'trash-can',
'tv': 'television',
'vacuum': 'robot-vacuum',
'washer': 'washing-machine',
}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Sense sensor."""
"""Set up the Sense binary sensor."""
if discovery_info is None:
return
@@ -67,14 +69,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def sense_to_mdi(sense_icon):
"""Convert sense icon to mdi icon."""
return 'mdi:' + MDI_ICONS.get(sense_icon, 'power-plug')
return 'mdi:{}'.format(MDI_ICONS.get(sense_icon, 'power-plug'))
class SenseDevice(BinarySensorDevice):
"""Implementation of a Sense energy device binary sensor."""
def __init__(self, data, device):
"""Initialize the sensor."""
"""Initialize the Sense binary sensor."""
self._name = device['name']
self._id = device['id']
self._icon = sense_to_mdi(device['icon'])

View File

@@ -41,6 +41,7 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
self._state = None
self._icon = None
self._battery = None
self._available = False
@property
def is_on(self):
@@ -71,6 +72,11 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
attr[ATTR_BATTERY_LEVEL] = self._battery
return attr
@property
def available(self):
"""Return True if entity is available."""
return self._available
def update(self):
"""Update the state."""
self.controller.get_states([self.tahoma_device])
@@ -82,11 +88,13 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
self._state = STATE_ON
if 'core:SensorDefectState' in self.tahoma_device.active_states:
# Set to 'lowBattery' for low battery warning.
# 'lowBattery' for low battery warning. 'dead' for not available.
self._battery = self.tahoma_device.active_states[
'core:SensorDefectState']
self._available = bool(self._battery != 'dead')
else:
self._battery = None
self._available = True
if self._state == STATE_ON:
self._icon = "mdi:fire"

View File

@@ -9,8 +9,9 @@ https://home-assistant.io/components/binary_sensor.tellduslive/
"""
import logging
from homeassistant.components.tellduslive import TelldusLiveEntity
from homeassistant.components import tellduslive
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.tellduslive.entry import TelldusLiveEntity
_LOGGER = logging.getLogger(__name__)
@@ -19,8 +20,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Tellstick sensors."""
if discovery_info is None:
return
client = hass.data[tellduslive.DOMAIN]
add_entities(
TelldusLiveSensor(hass, binary_sensor)
TelldusLiveSensor(client, binary_sensor)
for binary_sensor in discovery_info
)

View File

@@ -6,17 +6,19 @@ https://home-assistant.io/components/binary_sensor.volvooncall/
"""
import logging
from homeassistant.components.volvooncall import VolvoEntity
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY
from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASSES)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Volvo sensors."""
if discovery_info is None:
return
add_entities([VolvoSensor(hass, *discovery_info)])
async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)])
class VolvoSensor(VolvoEntity, BinarySensorDevice):
@@ -25,14 +27,11 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
@property
def is_on(self):
"""Return True if the binary sensor is on."""
val = getattr(self.vehicle, self._attribute)
if self._attribute == 'bulb_failures':
return bool(val)
if self._attribute in ['doors', 'windows']:
return any([val[key] for key in val if 'Open' in key])
return val != 'Normal'
return self.instrument.is_on
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return 'safety'
if self.instrument.device_class in DEVICE_CLASSES:
return self.instrument.device_class
return None

View File

@@ -7,7 +7,11 @@ at https://home-assistant.io/components/binary_sensor.zha/
import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
from homeassistant.components import zha
from homeassistant.components.zha import helpers
from homeassistant.components.zha.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
@@ -26,23 +30,43 @@ CLASS_MAPPING = {
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation binary sensors."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']:
await _async_setup_iaszone(hass, config, async_add_entities,
discovery_info)
elif OnOff.cluster_id in discovery_info['out_clusters']:
await _async_setup_remote(hass, config, async_add_entities,
discovery_info)
"""Old way of setting up Zigbee Home Automation binary sensors."""
pass
async def _async_setup_iaszone(hass, config, async_add_entities,
discovery_info):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation binary sensor from config entry."""
async def async_discover(discovery_info):
await _async_setup_entities(hass, config_entry, async_add_entities,
[discovery_info])
unsub = async_dispatcher_connect(
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
binary_sensors = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
if binary_sensors is not None:
await _async_setup_entities(hass, config_entry, async_add_entities,
binary_sensors.values())
del hass.data[DATA_ZHA][DOMAIN]
async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos):
"""Set up the ZHA binary sensors."""
entities = []
for discovery_info in discovery_infos:
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']:
entities.append(await _async_setup_iaszone(discovery_info))
elif OnOff.cluster_id in discovery_info['out_clusters']:
entities.append(await _async_setup_remote(discovery_info))
async_add_entities(entities, update_before_add=True)
async def _async_setup_iaszone(discovery_info):
device_class = None
from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
@@ -58,13 +82,10 @@ async def _async_setup_iaszone(hass, config, async_add_entities,
# If we fail to read from the device, use a non-specific class
pass
sensor = BinarySensor(device_class, **discovery_info)
async_add_entities([sensor], update_before_add=True)
return BinarySensor(device_class, **discovery_info)
async def _async_setup_remote(hass, config, async_add_entities,
discovery_info):
async def _async_setup_remote(discovery_info):
remote = Remote(**discovery_info)
if discovery_info['new_join']:
@@ -72,21 +93,21 @@ async def _async_setup_remote(hass, config, async_add_entities,
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await zha.configure_reporting(
await helpers.configure_reporting(
remote.entity_id, cluster, 0, min_report=0, max_report=600,
reportable_change=1
)
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
await zha.configure_reporting(
await helpers.configure_reporting(
remote.entity_id, cluster, 0, min_report=1, max_report=600,
reportable_change=1
)
async_add_entities([remote], update_before_add=True)
return remote
class BinarySensor(zha.Entity, BinarySensorDevice):
class BinarySensor(ZhaEntity, BinarySensorDevice):
"""The ZHA Binary Sensor."""
_domain = DOMAIN
@@ -130,16 +151,16 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
"""Retrieve latest state."""
from zigpy.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.ias_zone,
['zone_status'],
allow_cache=False,
only_cache=(not self._initialized))
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
class Remote(zha.Entity, BinarySensorDevice):
class Remote(ZhaEntity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN
@@ -252,7 +273,7 @@ class Remote(zha.Entity, BinarySensorDevice):
async def async_update(self):
"""Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff
result = await zha.safe_read(
result = await helpers.safe_read(
self._endpoint.out_clusters[OnOff.cluster_id],
['on_off'],
allow_cache=False,

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.10.3']
REQUIREMENTS = ['blinkpy==0.11.0']
_LOGGER = logging.getLogger(__name__)
@@ -111,7 +111,7 @@ def setup(hass, config):
def trigger_camera(call):
"""Trigger a camera."""
cameras = hass.data[BLINK_DATA].sync.cameras
cameras = hass.data[BLINK_DATA].cameras
name = call.data[CONF_NAME]
if name in cameras:
cameras[name].snap_picture()
@@ -148,7 +148,7 @@ async def async_handle_save_video_service(hass, call):
def _write_video(camera_name, video_path):
"""Call video write."""
all_cameras = hass.data[BLINK_DATA].sync.cameras
all_cameras = hass.data[BLINK_DATA].cameras
if camera_name in all_cameras:
all_cameras[camera_name].video_to_file(video_path)

View File

@@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return
data = hass.data[BLINK_DATA]
devs = []
for name, camera in data.sync.cameras.items():
for name, camera in data.cameras.items():
devs.append(BlinkCamera(data, name, camera))
add_entities(devs)

View File

@@ -60,13 +60,20 @@ async def async_setup_platform(hass, config, async_add_entities,
def extract_image_from_mjpeg(stream):
"""Take in a MJPEG stream object, return the jpg from it."""
data = b''
for chunk in stream:
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
jpg = data[jpg_start:jpg_end + 2]
return jpg
if jpg_end == -1:
continue
jpg_start = data.find(b'\xff\xd8')
if jpg_start == -1:
continue
return data[jpg_start:jpg_end + 2]
class MjpegCamera(Camera):

View File

@@ -10,14 +10,15 @@ import logging
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE, \
HTTP_HEADER_HA_AUTH
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from . import async_get_still_stream
from homeassistant.components.camera import async_get_still_stream
REQUIREMENTS = ['pillow==5.2.0']
REQUIREMENTS = ['pillow==5.3.0']
_LOGGER = logging.getLogger(__name__)
@@ -26,21 +27,34 @@ CONF_FORCE_RESIZE = 'force_resize'
CONF_IMAGE_QUALITY = 'image_quality'
CONF_IMAGE_REFRESH_RATE = 'image_refresh_rate'
CONF_MAX_IMAGE_WIDTH = 'max_image_width'
CONF_MAX_IMAGE_HEIGHT = 'max_image_height'
CONF_MAX_STREAM_WIDTH = 'max_stream_width'
CONF_MAX_STREAM_HEIGHT = 'max_stream_height'
CONF_IMAGE_TOP = 'image_top'
CONF_IMAGE_LEFT = 'image_left'
CONF_STREAM_QUALITY = 'stream_quality'
MODE_RESIZE = 'resize'
MODE_CROP = 'crop'
DEFAULT_BASENAME = "Camera Proxy"
DEFAULT_QUALITY = 75
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
vol.Optional(CONF_MODE, default=MODE_RESIZE):
vol.In([MODE_RESIZE, MODE_CROP]),
vol.Optional(CONF_IMAGE_QUALITY): int,
vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
vol.Optional(CONF_MAX_IMAGE_HEIGHT): int,
vol.Optional(CONF_MAX_STREAM_WIDTH): int,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MAX_STREAM_HEIGHT): int,
vol.Optional(CONF_IMAGE_LEFT): int,
vol.Optional(CONF_IMAGE_TOP): int,
vol.Optional(CONF_STREAM_QUALITY): int,
})
@@ -51,26 +65,37 @@ async def async_setup_platform(
async_add_entities([ProxyCamera(hass, config)])
def _precheck_image(image, opts):
"""Perform some pre-checks on the given image."""
from PIL import Image
import io
if not opts:
raise ValueError()
try:
img = Image.open(io.BytesIO(image))
except IOError:
_LOGGER.warning("Failed to open image")
raise ValueError()
imgfmt = str(img.format)
if imgfmt not in ('PNG', 'JPEG'):
_LOGGER.warning("Image is of unsupported type: %s", imgfmt)
raise ValueError()
return img
def _resize_image(image, opts):
"""Resize image."""
from PIL import Image
import io
if not opts:
try:
img = _precheck_image(image, opts)
except ValueError:
return image
quality = opts.quality or DEFAULT_QUALITY
new_width = opts.max_width
try:
img = Image.open(io.BytesIO(image))
except IOError:
return image
imgfmt = str(img.format)
if imgfmt not in ('PNG', 'JPEG'):
_LOGGER.debug("Image is of unsupported type: %s", imgfmt)
return image
(old_width, old_height) = img.size
old_size = len(image)
if old_width <= new_width:
@@ -87,7 +112,7 @@ def _resize_image(image, opts):
img.save(imgbuf, 'JPEG', optimize=True, quality=quality)
newimage = imgbuf.getvalue()
if not opts.force_resize and len(newimage) >= old_size:
_LOGGER.debug("Using original image(%d bytes) "
_LOGGER.debug("Using original image (%d bytes) "
"because resized image (%d bytes) is not smaller",
old_size, len(newimage))
return image
@@ -98,12 +123,50 @@ def _resize_image(image, opts):
return newimage
def _crop_image(image, opts):
"""Crop image."""
import io
try:
img = _precheck_image(image, opts)
except ValueError:
return image
quality = opts.quality or DEFAULT_QUALITY
(old_width, old_height) = img.size
old_size = len(image)
if opts.top is None:
opts.top = 0
if opts.left is None:
opts.left = 0
if opts.max_width is None or opts.max_width > old_width - opts.left:
opts.max_width = old_width - opts.left
if opts.max_height is None or opts.max_height > old_height - opts.top:
opts.max_height = old_height - opts.top
img = img.crop((opts.left, opts.top,
opts.left+opts.max_width, opts.top+opts.max_height))
imgbuf = io.BytesIO()
img.save(imgbuf, 'JPEG', optimize=True, quality=quality)
newimage = imgbuf.getvalue()
_LOGGER.debug(
"Cropped image from (%dx%d - %d bytes) to (%dx%d - %d bytes)",
old_width, old_height, old_size, opts.max_width, opts.max_height,
len(newimage))
return newimage
class ImageOpts():
"""The representation of image options."""
def __init__(self, max_width, quality, force_resize):
def __init__(self, max_width, max_height, left, top,
quality, force_resize):
"""Initialize image options."""
self.max_width = max_width
self.max_height = max_height
self.left = left
self.top = top
self.quality = quality
self.force_resize = force_resize
@@ -125,11 +188,18 @@ class ProxyCamera(Camera):
"{} - {}".format(DEFAULT_BASENAME, self._proxied_camera))
self._image_opts = ImageOpts(
config.get(CONF_MAX_IMAGE_WIDTH),
config.get(CONF_MAX_IMAGE_HEIGHT),
config.get(CONF_IMAGE_LEFT),
config.get(CONF_IMAGE_TOP),
config.get(CONF_IMAGE_QUALITY),
config.get(CONF_FORCE_RESIZE))
self._stream_opts = ImageOpts(
config.get(CONF_MAX_STREAM_WIDTH), config.get(CONF_STREAM_QUALITY),
config.get(CONF_MAX_STREAM_WIDTH),
config.get(CONF_MAX_STREAM_HEIGHT),
config.get(CONF_IMAGE_LEFT),
config.get(CONF_IMAGE_TOP),
config.get(CONF_STREAM_QUALITY),
True)
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
@@ -141,6 +211,7 @@ class ProxyCamera(Camera):
self._headers = (
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
if self.hass.config.api.api_password is not None else None)
self._mode = config.get(CONF_MODE)
def camera_image(self):
"""Return camera image."""
@@ -162,8 +233,12 @@ class ProxyCamera(Camera):
_LOGGER.error("Error getting original camera image")
return self._last_image
image = await self.hass.async_add_job(
_resize_image, image.content, self._image_opts)
if self._mode == MODE_RESIZE:
job = _resize_image
else:
job = _crop_image
image = await self.hass.async_add_executor_job(
job, image.content, self._image_opts)
if self._cache_images:
self._last_image = image
@@ -192,7 +267,11 @@ class ProxyCamera(Camera):
if not image:
return None
except HomeAssistantError:
raise asyncio.CancelledError
raise asyncio.CancelledError()
return await self.hass.async_add_job(
_resize_image, image.content, self._stream_opts)
if self._mode == MODE_RESIZE:
job = _resize_image
else:
job = _crop_image
return await self.hass.async_add_executor_job(
job, image.content, self._stream_opts)

View File

@@ -5,35 +5,33 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/camera.push/
"""
import logging
import asyncio
from collections import deque
from datetime import timedelta
import voluptuous as vol
import aiohttp
import async_timeout
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING
STATE_IDLE, STATE_RECORDING, DOMAIN
from homeassistant.core import callback
from homeassistant.components.http.view import KEY_AUTHENTICATED,\
HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\
HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['webhook']
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field'
CONF_TOKEN = 'token'
DEFAULT_NAME = "Push Camera"
ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip'
ATTR_TOKEN = 'token'
PUSH_CAMERA_DATA = 'push_camera'
@@ -43,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string,
vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)),
vol.Required(CONF_WEBHOOK_ID): cv.string,
})
@@ -53,69 +51,43 @@ async def async_setup_platform(hass, config, async_add_entities,
if PUSH_CAMERA_DATA not in hass.data:
hass.data[PUSH_CAMERA_DATA] = {}
cameras = [PushCamera(config[CONF_NAME],
webhook_id = config.get(CONF_WEBHOOK_ID)
cameras = [PushCamera(hass,
config[CONF_NAME],
config[CONF_BUFFER_SIZE],
config[CONF_TIMEOUT],
config.get(CONF_TOKEN))]
hass.http.register_view(CameraPushReceiver(hass,
config[CONF_IMAGE_FIELD]))
config[CONF_IMAGE_FIELD],
webhook_id)]
async_add_entities(cameras)
class CameraPushReceiver(HomeAssistantView):
"""Handle pushes from remote camera."""
async def handle_webhook(hass, webhook_id, request):
"""Handle incoming webhook POST with image files."""
try:
with async_timeout.timeout(5, loop=hass.loop):
data = dict(await request.post())
except (asyncio.TimeoutError, aiohttp.web.HTTPException) as error:
_LOGGER.error("Could not get information from POST <%s>", error)
return
url = "/api/camera_push/{entity_id}"
name = 'api:camera_push:camera_entity'
requires_auth = False
camera = hass.data[PUSH_CAMERA_DATA][webhook_id]
def __init__(self, hass, image_field):
"""Initialize CameraPushReceiver with camera entity."""
self._cameras = hass.data[PUSH_CAMERA_DATA]
self._image = image_field
if camera.image_field not in data:
_LOGGER.warning("Webhook call without POST parameter <%s>",
camera.image_field)
return
async def post(self, request, entity_id):
"""Accept the POST from Camera."""
_camera = self._cameras.get(entity_id)
if _camera is None:
_LOGGER.error("Unknown %s", entity_id)
status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\
else HTTP_UNAUTHORIZED
return self.json_message('Unknown {}'.format(entity_id),
status)
# Supports HA authentication and token based
# when token has been configured
authenticated = (request[KEY_AUTHENTICATED] or
(_camera.token is not None and
request.query.get('token') == _camera.token))
if not authenticated:
return self.json_message(
'Invalid authorization credentials for {}'.format(entity_id),
HTTP_UNAUTHORIZED)
try:
data = await request.post()
_LOGGER.debug("Received Camera push: %s", data[self._image])
await _camera.update_image(data[self._image].file.read(),
data[self._image].filename)
except ValueError as value_error:
_LOGGER.error("Unknown value %s", value_error)
return self.json_message('Invalid POST', HTTP_BAD_REQUEST)
except KeyError as key_error:
_LOGGER.error('In your POST message %s', key_error)
return self.json_message('{} missing'.format(self._image),
HTTP_BAD_REQUEST)
await camera.update_image(data[camera.image_field].file.read(),
data[camera.image_field].filename)
class PushCamera(Camera):
"""The representation of a Push camera."""
def __init__(self, name, buffer_size, timeout, token):
def __init__(self, hass, name, buffer_size, timeout, image_field,
webhook_id):
"""Initialize push camera component."""
super().__init__()
self._name = name
@@ -126,11 +98,28 @@ class PushCamera(Camera):
self._timeout = timeout
self.queue = deque([], buffer_size)
self._current_image = None
self.token = token
self._image_field = image_field
self.webhook_id = webhook_id
self.webhook_url = \
hass.components.webhook.async_generate_url(webhook_id)
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.data[PUSH_CAMERA_DATA][self.entity_id] = self
self.hass.data[PUSH_CAMERA_DATA][self.webhook_id] = self
try:
self.hass.components.webhook.async_register(DOMAIN,
self.name,
self.webhook_id,
handle_webhook)
except ValueError:
_LOGGER.error("In <%s>, webhook_id <%s> already used",
self.name, self.webhook_id)
@property
def image_field(self):
"""HTTP field containing the image file."""
return self._image_field
@property
def state(self):
@@ -189,6 +178,5 @@ class PushCamera(Camera):
name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename),
(ATTR_TOKEN, self.token),
) if value is not None
}

View File

@@ -10,12 +10,12 @@ import socket
import requests
import voluptuous as vol
from homeassistant.const import CONF_PORT
from homeassistant.const import CONF_PORT, CONF_SSL
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
REQUIREMENTS = ['uvcclient==0.10.1']
REQUIREMENTS = ['uvcclient==0.11.0']
_LOGGER = logging.getLogger(__name__)
@@ -25,12 +25,14 @@ CONF_PASSWORD = 'password'
DEFAULT_PASSWORD = 'ubnt'
DEFAULT_PORT = 7080
DEFAULT_SSL = False
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NVR): cv.string,
vol.Required(CONF_KEY): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
})
@@ -40,11 +42,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
key = config[CONF_KEY]
password = config[CONF_PASSWORD]
port = config[CONF_PORT]
ssl = config[CONF_SSL]
from uvcclient import nvr
try:
# Exceptions may be raised in all method calls to the nvr library.
nvrconn = nvr.UVCRemote(addr, port, key)
nvrconn = nvr.UVCRemote(addr, port, key, ssl=ssl)
cameras = nvrconn.index()
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'

View File

@@ -192,6 +192,11 @@ class DaikinClimate(ClimateDevice):
"""Return the name of the thermostat, if any."""
return self._api.name
@property
def unique_id(self):
"""Return a unique ID."""
return self._api.mac
@property
def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses."""

View File

@@ -1,7 +1,7 @@
"""Support for Honeywell evohome (EMEA/EU-based systems only).
"""Support for Climate devices of (EMEA/EU-based) Honeywell evohome systems.
Support for a temperature control system (TCS, controller) with 0+ heating
zones (e.g. TRVs, relays) and, optionally, a DHW controller.
zones (e.g. TRVs, relays).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.evohome/
@@ -13,29 +13,34 @@ import logging
from requests.exceptions import HTTPError
from homeassistant.components.climate import (
ClimateDevice,
STATE_AUTO,
STATE_ECO,
STATE_OFF,
SUPPORT_OPERATION_MODE,
STATE_AUTO, STATE_ECO, STATE_MANUAL, STATE_OFF,
SUPPORT_AWAY_MODE,
SUPPORT_ON_OFF,
SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE,
ClimateDevice
)
from homeassistant.components.evohome import (
CONF_LOCATION_IDX,
DATA_EVOHOME,
MAX_TEMP,
MIN_TEMP,
SCAN_INTERVAL_MAX
DATA_EVOHOME, DISPATCHER_EVOHOME,
CONF_LOCATION_IDX, SCAN_INTERVAL_DEFAULT,
EVO_PARENT, EVO_CHILD,
GWS, TCS,
)
from homeassistant.const import (
CONF_SCAN_INTERVAL,
PRECISION_TENTHS,
TEMP_CELSIUS,
HTTP_TOO_MANY_REQUESTS,
PRECISION_HALVES,
TEMP_CELSIUS
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (
dispatcher_send,
async_dispatcher_connect
)
_LOGGER = logging.getLogger(__name__)
# these are for the controller's opmode/state and the zone's state
# the Controller's opmode/state and the zone's (inherited) state
EVO_RESET = 'AutoWithReset'
EVO_AUTO = 'Auto'
EVO_AUTOECO = 'AutoWithEco'
@@ -44,7 +49,14 @@ EVO_DAYOFF = 'DayOff'
EVO_CUSTOM = 'Custom'
EVO_HEATOFF = 'HeatingOff'
EVO_STATE_TO_HA = {
# these are for Zones' opmode, and state
EVO_FOLLOW = 'FollowSchedule'
EVO_TEMPOVER = 'TemporaryOverride'
EVO_PERMOVER = 'PermanentOverride'
# for the Controller. NB: evohome treats Away mode as a mode in/of itself,
# where HA considers it to 'override' the exising operating mode
TCS_STATE_TO_HA = {
EVO_RESET: STATE_AUTO,
EVO_AUTO: STATE_AUTO,
EVO_AUTOECO: STATE_ECO,
@@ -53,171 +65,150 @@ EVO_STATE_TO_HA = {
EVO_CUSTOM: STATE_AUTO,
EVO_HEATOFF: STATE_OFF
}
HA_STATE_TO_EVO = {
HA_STATE_TO_TCS = {
STATE_AUTO: EVO_AUTO,
STATE_ECO: EVO_AUTOECO,
STATE_OFF: EVO_HEATOFF
}
TCS_OP_LIST = list(HA_STATE_TO_TCS)
HA_OP_LIST = list(HA_STATE_TO_EVO)
# the Zones' opmode; their state is usually 'inherited' from the TCS
EVO_FOLLOW = 'FollowSchedule'
EVO_TEMPOVER = 'TemporaryOverride'
EVO_PERMOVER = 'PermanentOverride'
# these are used to help prevent E501 (line too long) violations
GWS = 'gateways'
TCS = 'temperatureControlSystems'
# debug codes - these happen occasionally, but the cause is unknown
EVO_DEBUG_NO_RECENT_UPDATES = '0x01'
EVO_DEBUG_NO_STATUS = '0x02'
# for the Zones...
ZONE_STATE_TO_HA = {
EVO_FOLLOW: STATE_AUTO,
EVO_TEMPOVER: STATE_MANUAL,
EVO_PERMOVER: STATE_MANUAL
}
HA_STATE_TO_ZONE = {
STATE_AUTO: EVO_FOLLOW,
STATE_MANUAL: EVO_PERMOVER
}
ZONE_OP_LIST = list(HA_STATE_TO_ZONE)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create a Honeywell (EMEA/EU) evohome CH/DHW system.
An evohome system consists of: a controller, with 0-12 heating zones (e.g.
TRVs, relays) and, optionally, a DHW controller (a HW boiler).
Here, we add the controller only.
"""
async def async_setup_platform(hass, hass_config, async_add_entities,
discovery_info=None):
"""Create the evohome Controller, and its Zones, if any."""
evo_data = hass.data[DATA_EVOHOME]
client = evo_data['client']
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
# evohomeclient has no defined way of accessing non-default location other
# than using a protected member, such as below
# evohomeclient has exposed no means of accessing non-default location
# (i.e. loc_idx > 0) other than using a protected member, such as below
tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa E501; pylint: disable=protected-access
_LOGGER.debug(
"setup_platform(): Found Controller: id: %s [%s], type: %s",
"setup_platform(): Found Controller, id=%s [%s], "
"name=%s (location_idx=%s)",
tcs_obj_ref.systemId,
tcs_obj_ref.modelType,
tcs_obj_ref.location.name,
tcs_obj_ref.modelType
loc_idx
)
parent = EvoController(evo_data, client, tcs_obj_ref)
add_entities([parent], update_before_add=True)
controller = EvoController(evo_data, client, tcs_obj_ref)
zones = []
for zone_idx in tcs_obj_ref.zones:
zone_obj_ref = tcs_obj_ref.zones[zone_idx]
_LOGGER.debug(
"setup_platform(): Found Zone, id=%s [%s], "
"name=%s",
zone_obj_ref.zoneId,
zone_obj_ref.zone_type,
zone_obj_ref.name
)
zones.append(EvoZone(evo_data, client, zone_obj_ref))
entities = [controller] + zones
async_add_entities(entities, update_before_add=False)
class EvoController(ClimateDevice):
"""Base for a Honeywell evohome hub/Controller device.
class EvoClimateDevice(ClimateDevice):
"""Base for a Honeywell evohome Climate device."""
The Controller (aka TCS, temperature control system) is the parent of all
the child (CH/DHW) devices.
"""
# pylint: disable=no-member
def __init__(self, evo_data, client, obj_ref):
"""Initialize the evohome entity.
Most read-only properties are set here. So are pseudo read-only,
for example name (which _could_ change between update()s).
"""
self.client = client
"""Initialize the evohome entity."""
self._client = client
self._obj = obj_ref
self._id = obj_ref.systemId
self._name = evo_data['config']['locationInfo']['name']
self._config = evo_data['config'][GWS][0][TCS][0]
self._params = evo_data['params']
self._timers = evo_data['timers']
self._timers['statusUpdated'] = datetime.min
self._status = {}
self._available = False # should become True after first update()
def _handle_requests_exceptions(self, err):
# evohomeclient v2 api (>=0.2.7) exposes requests exceptions, incl.:
# - HTTP_BAD_REQUEST, is usually Bad user credentials
# - HTTP_TOO_MANY_REQUESTS, is api usuage limit exceeded
# - HTTP_SERVICE_UNAVAILABLE, is often Vendor's fault
async def async_added_to_hass(self):
"""Run when entity about to be added."""
async_dispatcher_connect(self.hass, DISPATCHER_EVOHOME, self._connect)
@callback
def _connect(self, packet):
if packet['to'] & self._type and packet['signal'] == 'refresh':
self.async_schedule_update_ha_state(force_refresh=True)
def _handle_requests_exceptions(self, err):
if err.response.status_code == HTTP_TOO_MANY_REQUESTS:
# execute a back off: pause, and reduce rate
old_scan_interval = self._params[CONF_SCAN_INTERVAL]
new_scan_interval = min(old_scan_interval * 2, SCAN_INTERVAL_MAX)
self._params[CONF_SCAN_INTERVAL] = new_scan_interval
# execute a backoff: pause, and also reduce rate
old_interval = self._params[CONF_SCAN_INTERVAL]
new_interval = min(old_interval, SCAN_INTERVAL_DEFAULT) * 2
self._params[CONF_SCAN_INTERVAL] = new_interval
_LOGGER.warning(
"API rate limit has been exceeded: increasing '%s' from %s to "
"%s seconds, and suspending polling for %s seconds.",
"API rate limit has been exceeded. Suspending polling for %s "
"seconds, and increasing '%s' from %s to %s seconds.",
new_interval * 3,
CONF_SCAN_INTERVAL,
old_scan_interval,
new_scan_interval,
new_scan_interval * 3
old_interval,
new_interval,
)
self._timers['statusUpdated'] = datetime.now() + \
timedelta(seconds=new_scan_interval * 3)
self._timers['statusUpdated'] = datetime.now() + new_interval * 3
else:
raise err
raise err # we dont handle any other HTTPErrors
@property
def name(self):
def name(self) -> str:
"""Return the name to use in the frontend UI."""
return self._name
@property
def available(self):
"""Return True if the device is available.
def icon(self):
"""Return the icon to use in the frontend UI."""
return self._icon
All evohome entities are initially unavailable. Once HA has started,
state data is then retrieved by the Controller, and then the children
will get a state (e.g. operating_mode, current_temperature).
@property
def device_state_attributes(self):
"""Return the device state attributes of the evohome Climate device.
However, evohome entities can become unavailable for other reasons.
This is state data that is not available otherwise, due to the
restrictions placed upon ClimateDevice properties, etc. by HA.
"""
return {'status': self._status}
@property
def available(self) -> bool:
"""Return True if the device is currently available."""
return self._available
@property
def supported_features(self):
"""Get the list of supported features of the Controller."""
return SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE
@property
def device_state_attributes(self):
"""Return the device state attributes of the controller.
This is operating mode state data that is not available otherwise, due
to the restrictions placed upon ClimateDevice properties, etc by HA.
"""
data = {}
data['systemMode'] = self._status['systemModeStatus']['mode']
data['isPermanent'] = self._status['systemModeStatus']['isPermanent']
if 'timeUntil' in self._status['systemModeStatus']:
data['timeUntil'] = self._status['systemModeStatus']['timeUntil']
data['activeFaults'] = self._status['activeFaults']
return data
"""Get the list of supported features of the device."""
return self._supported_features
@property
def operation_list(self):
"""Return the list of available operations."""
return HA_OP_LIST
@property
def current_operation(self):
"""Return the operation mode of the evohome entity."""
return EVO_STATE_TO_HA.get(self._status['systemModeStatus']['mode'])
@property
def target_temperature(self):
"""Return the average target temperature of the Heating/DHW zones."""
temps = [zone['setpointStatus']['targetHeatTemperature']
for zone in self._status['zones']]
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
return avg_temp
@property
def current_temperature(self):
"""Return the average current temperature of the Heating/DHW zones."""
tmp_list = [x for x in self._status['zones']
if x['temperatureStatus']['isAvailable'] is True]
temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list]
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
return avg_temp
return self._operation_list
@property
def temperature_unit(self):
@@ -227,47 +218,313 @@ class EvoController(ClimateDevice):
@property
def precision(self):
"""Return the temperature precision to use in the frontend UI."""
return PRECISION_TENTHS
return PRECISION_HALVES
class EvoZone(EvoClimateDevice):
"""Base for a Honeywell evohome Zone device."""
def __init__(self, evo_data, client, obj_ref):
"""Initialize the evohome Zone."""
super().__init__(evo_data, client, obj_ref)
self._id = obj_ref.zoneId
self._name = obj_ref.name
self._icon = "mdi:radiator"
self._type = EVO_CHILD
for _zone in evo_data['config'][GWS][0][TCS][0]['zones']:
if _zone['zoneId'] == self._id:
self._config = _zone
break
self._status = {}
self._operation_list = ZONE_OP_LIST
self._supported_features = \
SUPPORT_OPERATION_MODE | \
SUPPORT_TARGET_TEMPERATURE | \
SUPPORT_ON_OFF
@property
def min_temp(self):
"""Return the minimum target temp (setpoint) of a evohome entity."""
return MIN_TEMP
"""Return the minimum target temperature of a evohome Zone.
The default is 5 (in Celsius), but it is configurable within 5-35.
"""
return self._config['setpointCapabilities']['minHeatSetpoint']
@property
def max_temp(self):
"""Return the maximum target temp (setpoint) of a evohome entity."""
return MAX_TEMP
"""Return the minimum target temperature of a evohome Zone.
The default is 35 (in Celsius), but it is configurable within 5-35.
"""
return self._config['setpointCapabilities']['maxHeatSetpoint']
@property
def is_on(self):
"""Return true as evohome controllers are always on.
def target_temperature(self):
"""Return the target temperature of the evohome Zone."""
return self._status['setpointStatus']['targetHeatTemperature']
Operating modes can include 'HeatingOff', but (for example) DHW would
remain on.
@property
def current_temperature(self):
"""Return the current temperature of the evohome Zone."""
return self._status['temperatureStatus']['temperature']
@property
def current_operation(self):
"""Return the current operating mode of the evohome Zone.
The evohome Zones that are in 'FollowSchedule' mode inherit their
actual operating mode from the Controller.
"""
evo_data = self.hass.data[DATA_EVOHOME]
system_mode = evo_data['status']['systemModeStatus']['mode']
setpoint_mode = self._status['setpointStatus']['setpointMode']
if setpoint_mode == EVO_FOLLOW:
# then inherit state from the controller
if system_mode == EVO_RESET:
current_operation = TCS_STATE_TO_HA.get(EVO_AUTO)
else:
current_operation = TCS_STATE_TO_HA.get(system_mode)
else:
current_operation = ZONE_STATE_TO_HA.get(setpoint_mode)
return current_operation
@property
def is_on(self) -> bool:
"""Return True if the evohome Zone is off.
A Zone is considered off if its target temp is set to its minimum, and
it is not following its schedule (i.e. not in 'FollowSchedule' mode).
"""
is_off = \
self.target_temperature == self.min_temp and \
self._status['setpointStatus']['setpointMode'] == EVO_PERMOVER
return not is_off
def _set_temperature(self, temperature, until=None):
"""Set the new target temperature of a Zone.
temperature is required, until can be:
- strftime('%Y-%m-%dT%H:%M:%SZ') for TemporaryOverride, or
- None for PermanentOverride (i.e. indefinitely)
"""
try:
self._obj.set_temperature(temperature, until)
except HTTPError as err:
self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member
def set_temperature(self, **kwargs):
"""Set new target temperature, indefinitely."""
self._set_temperature(kwargs['temperature'], until=None)
def turn_on(self):
"""Turn the evohome Zone on.
This is achieved by setting the Zone to its 'FollowSchedule' mode.
"""
self._set_operation_mode(EVO_FOLLOW)
def turn_off(self):
"""Turn the evohome Zone off.
This is achieved by setting the Zone to its minimum temperature,
indefinitely (i.e. 'PermanentOverride' mode).
"""
self._set_temperature(self.min_temp, until=None)
def set_operation_mode(self, operation_mode):
"""Set an operating mode for a Zone.
Currently limited to 'Auto' & 'Manual'. If 'Off' is needed, it can be
enabled via turn_off method.
NB: evohome Zones do not have an operating mode as understood by HA.
Instead they usually 'inherit' an operating mode from their controller.
More correctly, these Zones are in a follow mode, 'FollowSchedule',
where their setpoint temperatures are a function of their schedule, and
the Controller's operating_mode, e.g. Economy mode is their scheduled
setpoint less (usually) 3C.
Thus, you cannot set a Zone to Away mode, but the location (i.e. the
Controller) is set to Away and each Zones's setpoints are adjusted
accordingly to some lower temperature.
However, Zones can override these setpoints, either for a specified
period of time, 'TemporaryOverride', after which they will revert back
to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'.
"""
self._set_operation_mode(HA_STATE_TO_ZONE.get(operation_mode))
def _set_operation_mode(self, operation_mode):
if operation_mode == EVO_FOLLOW:
try:
self._obj.cancel_temp_override(self._obj)
except HTTPError as err:
self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member
elif operation_mode == EVO_TEMPOVER:
_LOGGER.error(
"_set_operation_mode(op_mode=%s): mode not yet implemented",
operation_mode
)
elif operation_mode == EVO_PERMOVER:
self._set_temperature(self.target_temperature, until=None)
else:
_LOGGER.error(
"_set_operation_mode(op_mode=%s): mode not valid",
operation_mode
)
@property
def should_poll(self) -> bool:
"""Return False as evohome child devices should never be polled.
The evohome Controller will inform its children when to update().
"""
return False
def update(self):
"""Process the evohome Zone's state data."""
evo_data = self.hass.data[DATA_EVOHOME]
for _zone in evo_data['status']['zones']:
if _zone['zoneId'] == self._id:
self._status = _zone
break
self._available = True
class EvoController(EvoClimateDevice):
"""Base for a Honeywell evohome hub/Controller device.
The Controller (aka TCS, temperature control system) is the parent of all
the child (CH/DHW) devices. It is also a Climate device.
"""
def __init__(self, evo_data, client, obj_ref):
"""Initialize the evohome Controller (hub)."""
super().__init__(evo_data, client, obj_ref)
self._id = obj_ref.systemId
self._name = '_{}'.format(obj_ref.location.name)
self._icon = "mdi:thermostat"
self._type = EVO_PARENT
self._config = evo_data['config'][GWS][0][TCS][0]
self._status = evo_data['status']
self._timers['statusUpdated'] = datetime.min
self._operation_list = TCS_OP_LIST
self._supported_features = \
SUPPORT_OPERATION_MODE | \
SUPPORT_AWAY_MODE
@property
def device_state_attributes(self):
"""Return the device state attributes of the evohome Controller.
This is state data that is not available otherwise, due to the
restrictions placed upon ClimateDevice properties, etc. by HA.
"""
status = dict(self._status)
if 'zones' in status:
del status['zones']
if 'dhw' in status:
del status['dhw']
return {'status': status}
@property
def current_operation(self):
"""Return the current operating mode of the evohome Controller."""
return TCS_STATE_TO_HA.get(self._status['systemModeStatus']['mode'])
@property
def min_temp(self):
"""Return the minimum target temperature of a evohome Controller.
Although evohome Controllers do not have a minimum target temp, one is
expected by the HA schema; the default for an evohome HR92 is used.
"""
return 5
@property
def max_temp(self):
"""Return the minimum target temperature of a evohome Controller.
Although evohome Controllers do not have a maximum target temp, one is
expected by the HA schema; the default for an evohome HR92 is used.
"""
return 35
@property
def target_temperature(self):
"""Return the average target temperature of the Heating/DHW zones.
Although evohome Controllers do not have a target temp, one is
expected by the HA schema.
"""
temps = [zone['setpointStatus']['targetHeatTemperature']
for zone in self._status['zones']]
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
return avg_temp
@property
def current_temperature(self):
"""Return the average current temperature of the Heating/DHW zones.
Although evohome Controllers do not have a target temp, one is
expected by the HA schema.
"""
tmp_list = [x for x in self._status['zones']
if x['temperatureStatus']['isAvailable'] is True]
temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list]
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
return avg_temp
@property
def is_on(self) -> bool:
"""Return True as evohome Controllers are always on.
For example, evohome Controllers have a 'HeatingOff' mode, but even
then the DHW would remain on.
"""
return True
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
def is_away_mode_on(self) -> bool:
"""Return True if away mode is on."""
return self._status['systemModeStatus']['mode'] == EVO_AWAY
def turn_away_mode_on(self):
"""Turn away mode on."""
"""Turn away mode on.
The evohome Controller will not remember is previous operating mode.
"""
self._set_operation_mode(EVO_AWAY)
def turn_away_mode_off(self):
"""Turn away mode off."""
"""Turn away mode off.
The evohome Controller can not recall its previous operating mode (as
intimated by the HA schema), so this method is achieved by setting the
Controller's mode back to Auto.
"""
self._set_operation_mode(EVO_AUTO)
def _set_operation_mode(self, operation_mode):
# Set new target operation mode for the TCS.
_LOGGER.debug(
"_set_operation_mode(): API call [1 request(s)]: "
"tcs._set_status(%s)...",
operation_mode
)
try:
self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access
except HTTPError as err:
@@ -279,93 +536,45 @@ class EvoController(ClimateDevice):
Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away'
mode is needed, it can be enabled via turn_away_mode_on method.
"""
self._set_operation_mode(HA_STATE_TO_EVO.get(operation_mode))
self._set_operation_mode(HA_STATE_TO_TCS.get(operation_mode))
def _update_state_data(self, evo_data):
client = evo_data['client']
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
_LOGGER.debug(
"_update_state_data(): API call [1 request(s)]: "
"client.locations[loc_idx].status()..."
)
try:
evo_data['status'].update(
client.locations[loc_idx].status()[GWS][0][TCS][0])
except HTTPError as err: # check if we've exceeded the api rate limit
self._handle_requests_exceptions(err)
else:
evo_data['timers']['statusUpdated'] = datetime.now()
_LOGGER.debug(
"_update_state_data(): evo_data['status'] = %s",
evo_data['status']
)
@property
def should_poll(self) -> bool:
"""Return True as the evohome Controller should always be polled."""
return True
def update(self):
"""Get the latest state data of the installation.
"""Get the latest state data of the entire evohome Location.
This includes state data for the Controller and its child devices, such
as the operating_mode of the Controller and the current_temperature
of its children.
This is not asyncio-friendly due to the underlying client api.
This includes state data for the Controller and all its child devices,
such as the operating mode of the Controller and the current temp of
its children (e.g. Zones, DHW controller).
"""
evo_data = self.hass.data[DATA_EVOHOME]
# should the latest evohome state data be retreived this cycle?
timeout = datetime.now() + timedelta(seconds=55)
expired = timeout > self._timers['statusUpdated'] + \
timedelta(seconds=evo_data['params'][CONF_SCAN_INTERVAL])
self._params[CONF_SCAN_INTERVAL]
if not expired:
return
was_available = self._available or \
self._timers['statusUpdated'] == datetime.min
self._update_state_data(evo_data)
self._status = evo_data['status']
if _LOGGER.isEnabledFor(logging.DEBUG):
tmp_dict = dict(self._status)
if 'zones' in tmp_dict:
tmp_dict['zones'] = '...'
if 'dhw' in tmp_dict:
tmp_dict['dhw'] = '...'
_LOGGER.debug(
"update(%s), self._status = %s",
self._id + " [" + self._name + "]",
tmp_dict
)
no_recent_updates = self._timers['statusUpdated'] < datetime.now() - \
timedelta(seconds=self._params[CONF_SCAN_INTERVAL] * 3.1)
if no_recent_updates:
self._available = False
debug_code = EVO_DEBUG_NO_RECENT_UPDATES
elif not self._status:
# unavailable because no status (but how? other than at startup?)
self._available = False
debug_code = EVO_DEBUG_NO_STATUS
# Retreive the latest state data via the client api
loc_idx = self._params[CONF_LOCATION_IDX]
try:
self._status.update(
self._client.locations[loc_idx].status()[GWS][0][TCS][0])
except HTTPError as err: # check if we've exceeded the api rate limit
self._handle_requests_exceptions(err)
else:
self._timers['statusUpdated'] = datetime.now()
self._available = True
if not self._available and was_available:
# only warn if available went from True to False
_LOGGER.warning(
"The entity, %s, has become unavailable, debug code is: %s",
self._id + " [" + self._name + "]",
debug_code
)
_LOGGER.debug(
"_update_state_data(): self._status = %s",
self._status
)
elif self._available and not was_available:
# this isn't the first re-available (e.g. _after_ STARTUP)
_LOGGER.debug(
"The entity, %s, has become available",
self._id + " [" + self._name + "]"
)
# inform the child devices that state data has been updated
pkt = {'sender': 'controller', 'signal': 'refresh', 'to': EVO_CHILD}
dispatcher_send(self.hass, DISPATCHER_EVOHOME, pkt)

View File

@@ -23,7 +23,7 @@ from homeassistant.helpers import condition
from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.restore_state import RestoreEntity
_LOGGER = logging.getLogger(__name__)
@@ -96,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities,
precision)])
class GenericThermostat(ClimateDevice):
class GenericThermostat(ClimateDevice, RestoreEntity):
"""Representation of a Generic Thermostat device."""
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
@@ -155,8 +155,9 @@ class GenericThermostat(ClimateDevice):
async def async_added_to_hass(self):
"""Run when entity about to be added."""
await super().async_added_to_hass()
# Check If we have an old state
old_state = await async_get_last_state(self.hass, self.entity_id)
old_state = await self.async_get_last_state()
if old_state is not None:
# If we have no initial temperature, restore
if self._target_temp is None:

View File

@@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE, CONF_REGION)
REQUIREMENTS = ['evohomeclient==0.2.7', 'somecomfort==0.5.2']
REQUIREMENTS = ['evohomeclient==0.2.8', 'somecomfort==0.5.2']
_LOGGER = logging.getLogger(__name__)

View File

@@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['millheater==0.2.8']
REQUIREMENTS = ['millheater==0.2.9']
_LOGGER = logging.getLogger(__name__)

View File

@@ -22,7 +22,8 @@ from homeassistant.const import (
from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate)
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate,
subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -77,6 +78,18 @@ CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp'
CONF_TEMP_STEP = 'temp_step'
TEMPLATE_KEYS = (
CONF_POWER_STATE_TEMPLATE,
CONF_MODE_STATE_TEMPLATE,
CONF_TEMPERATURE_STATE_TEMPLATE,
CONF_FAN_MODE_STATE_TEMPLATE,
CONF_SWING_MODE_STATE_TEMPLATE,
CONF_AWAY_MODE_STATE_TEMPLATE,
CONF_HOLD_STATE_TEMPLATE,
CONF_AUX_STATE_TEMPLATE,
CONF_CURRENT_TEMPERATURE_TEMPLATE
)
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
@@ -153,69 +166,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None):
"""Set up the MQTT climate devices."""
template_keys = (
CONF_POWER_STATE_TEMPLATE,
CONF_MODE_STATE_TEMPLATE,
CONF_TEMPERATURE_STATE_TEMPLATE,
CONF_FAN_MODE_STATE_TEMPLATE,
CONF_SWING_MODE_STATE_TEMPLATE,
CONF_AWAY_MODE_STATE_TEMPLATE,
CONF_HOLD_STATE_TEMPLATE,
CONF_AUX_STATE_TEMPLATE,
CONF_CURRENT_TEMPERATURE_TEMPLATE
)
value_templates = {}
if CONF_VALUE_TEMPLATE in config:
value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass
value_templates = {key: value_template for key in template_keys}
for key in template_keys & config.keys():
value_templates[key] = config.get(key)
value_templates[key].hass = hass
async_add_entities([
MqttClimate(
hass,
config.get(CONF_NAME),
{
key: config.get(key) for key in (
CONF_POWER_COMMAND_TOPIC,
CONF_MODE_COMMAND_TOPIC,
CONF_TEMPERATURE_COMMAND_TOPIC,
CONF_FAN_MODE_COMMAND_TOPIC,
CONF_SWING_MODE_COMMAND_TOPIC,
CONF_AWAY_MODE_COMMAND_TOPIC,
CONF_HOLD_COMMAND_TOPIC,
CONF_AUX_COMMAND_TOPIC,
CONF_POWER_STATE_TOPIC,
CONF_MODE_STATE_TOPIC,
CONF_TEMPERATURE_STATE_TOPIC,
CONF_FAN_MODE_STATE_TOPIC,
CONF_SWING_MODE_STATE_TOPIC,
CONF_AWAY_MODE_STATE_TOPIC,
CONF_HOLD_STATE_TOPIC,
CONF_AUX_STATE_TOPIC,
CONF_CURRENT_TEMPERATURE_TOPIC
)
},
value_templates,
config.get(CONF_QOS),
config.get(CONF_RETAIN),
config.get(CONF_MODE_LIST),
config.get(CONF_FAN_MODE_LIST),
config.get(CONF_SWING_MODE_LIST),
config.get(CONF_INITIAL),
False, None, SPEED_LOW,
STATE_OFF, STATE_OFF, False,
config.get(CONF_SEND_IF_OFF),
config.get(CONF_PAYLOAD_ON),
config.get(CONF_PAYLOAD_OFF),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_MIN_TEMP),
config.get(CONF_MAX_TEMP),
config.get(CONF_TEMP_STEP),
config,
discovery_hash,
)])
@@ -223,54 +177,103 @@ async def _async_setup_entity(hass, config, async_add_entities,
class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
"""Representation of an MQTT climate device."""
def __init__(self, hass, name, topic, value_templates, qos, retain,
mode_list, fan_mode_list, swing_mode_list,
target_temperature, away, hold, current_fan_mode,
current_swing_mode, current_operation, aux, send_if_off,
payload_on, payload_off, availability_topic,
payload_available, payload_not_available,
min_temp, max_temp, temp_step, discovery_hash):
def __init__(self, hass, config, discovery_hash):
"""Initialize the climate device."""
self._config = config
self._sub_state = None
self.hass = hass
self._topic = None
self._value_templates = None
self._target_temperature = None
self._current_fan_mode = None
self._current_operation = None
self._current_swing_mode = None
self._unit_of_measurement = hass.config.units.temperature_unit
self._away = False
self._hold = None
self._current_temperature = None
self._aux = False
self._setup_from_config(config)
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS)
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
self.hass = hass
self._name = name
self._topic = topic
self._value_templates = value_templates
self._qos = qos
self._retain = retain
MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update)
async def async_added_to_hass(self):
"""Handle being added to home assistant."""
await super().async_added_to_hass()
await self._subscribe_topics()
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
self._setup_from_config(config)
await self.availability_discovery_update(config)
await self._subscribe_topics()
self.async_schedule_update_ha_state()
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
self._topic = {
key: config.get(key) for key in (
CONF_POWER_COMMAND_TOPIC,
CONF_MODE_COMMAND_TOPIC,
CONF_TEMPERATURE_COMMAND_TOPIC,
CONF_FAN_MODE_COMMAND_TOPIC,
CONF_SWING_MODE_COMMAND_TOPIC,
CONF_AWAY_MODE_COMMAND_TOPIC,
CONF_HOLD_COMMAND_TOPIC,
CONF_AUX_COMMAND_TOPIC,
CONF_POWER_STATE_TOPIC,
CONF_MODE_STATE_TOPIC,
CONF_TEMPERATURE_STATE_TOPIC,
CONF_FAN_MODE_STATE_TOPIC,
CONF_SWING_MODE_STATE_TOPIC,
CONF_AWAY_MODE_STATE_TOPIC,
CONF_HOLD_STATE_TOPIC,
CONF_AUX_STATE_TOPIC,
CONF_CURRENT_TEMPERATURE_TOPIC
)
}
# set to None in non-optimistic mode
self._target_temperature = self._current_fan_mode = \
self._current_operation = self._current_swing_mode = None
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
self._target_temperature = target_temperature
self._unit_of_measurement = hass.config.units.temperature_unit
self._away = away
self._hold = hold
self._current_temperature = None
self._target_temperature = config.get(CONF_INITIAL)
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
self._current_fan_mode = current_fan_mode
if self._topic[CONF_MODE_STATE_TOPIC] is None:
self._current_operation = current_operation
self._aux = aux
self._current_fan_mode = SPEED_LOW
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
self._current_swing_mode = current_swing_mode
self._fan_list = fan_mode_list
self._operation_list = mode_list
self._swing_list = swing_mode_list
self._target_temperature_step = temp_step
self._send_if_off = send_if_off
self._payload_on = payload_on
self._payload_off = payload_off
self._min_temp = min_temp
self._max_temp = max_temp
self._discovery_hash = discovery_hash
self._current_swing_mode = STATE_OFF
if self._topic[CONF_MODE_STATE_TOPIC] is None:
self._current_operation = STATE_OFF
self._away = False
self._hold = None
self._aux = False
async def async_added_to_hass(self):
"""Handle being added to home assistant."""
await MqttAvailability.async_added_to_hass(self)
await MqttDiscoveryUpdate.async_added_to_hass(self)
value_templates = {}
if CONF_VALUE_TEMPLATE in config:
value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = self.hass
value_templates = {key: value_template for key in TEMPLATE_KEYS}
for key in TEMPLATE_KEYS & config.keys():
value_templates[key] = config.get(key)
value_templates[key].hass = self.hass
self._value_templates = value_templates
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
topics = {}
qos = self._config.get(CONF_QOS)
@callback
def handle_current_temp_received(topic, payload, qos):
@@ -287,9 +290,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
_LOGGER.error("Could not parse temperature from %s", payload)
if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
handle_current_temp_received, self._qos)
topics[CONF_CURRENT_TEMPERATURE_TOPIC] = {
'topic': self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
'msg_callback': handle_current_temp_received,
'qos': qos}
@callback
def handle_mode_received(topic, payload, qos):
@@ -298,16 +302,17 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
payload = self._value_templates[CONF_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)
if payload not in self._operation_list:
if payload not in self._config.get(CONF_MODE_LIST):
_LOGGER.error("Invalid mode: %s", payload)
else:
self._current_operation = payload
self.async_schedule_update_ha_state()
if self._topic[CONF_MODE_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_MODE_STATE_TOPIC],
handle_mode_received, self._qos)
topics[CONF_MODE_STATE_TOPIC] = {
'topic': self._topic[CONF_MODE_STATE_TOPIC],
'msg_callback': handle_mode_received,
'qos': qos}
@callback
def handle_temperature_received(topic, payload, qos):
@@ -324,9 +329,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
_LOGGER.error("Could not parse temperature from %s", payload)
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC],
handle_temperature_received, self._qos)
topics[CONF_TEMPERATURE_STATE_TOPIC] = {
'topic': self._topic[CONF_TEMPERATURE_STATE_TOPIC],
'msg_callback': handle_temperature_received,
'qos': qos}
@callback
def handle_fan_mode_received(topic, payload, qos):
@@ -336,16 +342,17 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self._value_templates[CONF_FAN_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)
if payload not in self._fan_list:
if payload not in self._config.get(CONF_FAN_MODE_LIST):
_LOGGER.error("Invalid fan mode: %s", payload)
else:
self._current_fan_mode = payload
self.async_schedule_update_ha_state()
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC],
handle_fan_mode_received, self._qos)
topics[CONF_FAN_MODE_STATE_TOPIC] = {
'topic': self._topic[CONF_FAN_MODE_STATE_TOPIC],
'msg_callback': handle_fan_mode_received,
'qos': qos}
@callback
def handle_swing_mode_received(topic, payload, qos):
@@ -355,32 +362,35 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self._value_templates[CONF_SWING_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)
if payload not in self._swing_list:
if payload not in self._config.get(CONF_SWING_MODE_LIST):
_LOGGER.error("Invalid swing mode: %s", payload)
else:
self._current_swing_mode = payload
self.async_schedule_update_ha_state()
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC],
handle_swing_mode_received, self._qos)
topics[CONF_SWING_MODE_STATE_TOPIC] = {
'topic': self._topic[CONF_SWING_MODE_STATE_TOPIC],
'msg_callback': handle_swing_mode_received,
'qos': qos}
@callback
def handle_away_mode_received(topic, payload, qos):
"""Handle receiving away mode via MQTT."""
payload_on = self._config.get(CONF_PAYLOAD_ON)
payload_off = self._config.get(CONF_PAYLOAD_OFF)
if CONF_AWAY_MODE_STATE_TEMPLATE in self._value_templates:
payload = \
self._value_templates[CONF_AWAY_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)
if payload == "True":
payload = self._payload_on
payload = payload_on
elif payload == "False":
payload = self._payload_off
payload = payload_off
if payload == self._payload_on:
if payload == payload_on:
self._away = True
elif payload == self._payload_off:
elif payload == payload_off:
self._away = False
else:
_LOGGER.error("Invalid away mode: %s", payload)
@@ -388,24 +398,27 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self.async_schedule_update_ha_state()
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC],
handle_away_mode_received, self._qos)
topics[CONF_AWAY_MODE_STATE_TOPIC] = {
'topic': self._topic[CONF_AWAY_MODE_STATE_TOPIC],
'msg_callback': handle_away_mode_received,
'qos': qos}
@callback
def handle_aux_mode_received(topic, payload, qos):
"""Handle receiving aux mode via MQTT."""
payload_on = self._config.get(CONF_PAYLOAD_ON)
payload_off = self._config.get(CONF_PAYLOAD_OFF)
if CONF_AUX_STATE_TEMPLATE in self._value_templates:
payload = self._value_templates[CONF_AUX_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload)
if payload == "True":
payload = self._payload_on
payload = payload_on
elif payload == "False":
payload = self._payload_off
payload = payload_off
if payload == self._payload_on:
if payload == payload_on:
self._aux = True
elif payload == self._payload_off:
elif payload == payload_off:
self._aux = False
else:
_LOGGER.error("Invalid aux mode: %s", payload)
@@ -413,9 +426,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self.async_schedule_update_ha_state()
if self._topic[CONF_AUX_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_AUX_STATE_TOPIC],
handle_aux_mode_received, self._qos)
topics[CONF_AUX_STATE_TOPIC] = {
'topic': self._topic[CONF_AUX_STATE_TOPIC],
'msg_callback': handle_aux_mode_received,
'qos': qos}
@callback
def handle_hold_mode_received(topic, payload, qos):
@@ -428,9 +442,19 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self.async_schedule_update_ha_state()
if self._topic[CONF_HOLD_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_HOLD_STATE_TOPIC],
handle_hold_mode_received, self._qos)
topics[CONF_HOLD_STATE_TOPIC] = {
'topic': self._topic[CONF_HOLD_STATE_TOPIC],
'msg_callback': handle_hold_mode_received,
'qos': qos}
self._sub_state = await subscription.async_subscribe_topics(
self.hass, self._sub_state,
topics)
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
await MqttAvailability.async_will_remove_from_hass(self)
@property
def should_poll(self):
@@ -440,7 +464,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property
def name(self):
"""Return the name of the climate device."""
return self._name
return self._config.get(CONF_NAME)
@property
def temperature_unit(self):
@@ -465,12 +489,12 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property
def operation_list(self):
"""Return the list of available operation modes."""
return self._operation_list
return self._config.get(CONF_MODE_LIST)
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return self._target_temperature_step
return self._config.get(CONF_TEMP_STEP)
@property
def is_away_mode_on(self):
@@ -495,7 +519,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property
def fan_list(self):
"""Return the list of available fan modes."""
return self._fan_list
return self._config.get(CONF_FAN_MODE_LIST)
async def async_set_temperature(self, **kwargs):
"""Set new target temperatures."""
@@ -508,19 +532,23 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
# optimistic mode
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
if self._send_if_off or self._current_operation != STATE_OFF:
if (self._config.get(CONF_SEND_IF_OFF) or
self._current_operation != STATE_OFF):
mqtt.async_publish(
self.hass, self._topic[CONF_TEMPERATURE_COMMAND_TOPIC],
kwargs.get(ATTR_TEMPERATURE), self._qos, self._retain)
kwargs.get(ATTR_TEMPERATURE), self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
self.async_schedule_update_ha_state()
async def async_set_swing_mode(self, swing_mode):
"""Set new swing mode."""
if self._send_if_off or self._current_operation != STATE_OFF:
if (self._config.get(CONF_SEND_IF_OFF) or
self._current_operation != STATE_OFF):
mqtt.async_publish(
self.hass, self._topic[CONF_SWING_MODE_COMMAND_TOPIC],
swing_mode, self._qos, self._retain)
swing_mode, self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
self._current_swing_mode = swing_mode
@@ -528,10 +556,12 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature."""
if self._send_if_off or self._current_operation != STATE_OFF:
if (self._config.get(CONF_SEND_IF_OFF) or
self._current_operation != STATE_OFF):
mqtt.async_publish(
self.hass, self._topic[CONF_FAN_MODE_COMMAND_TOPIC],
fan_mode, self._qos, self._retain)
fan_mode, self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
self._current_fan_mode = fan_mode
@@ -539,22 +569,24 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
async def async_set_operation_mode(self, operation_mode) -> None:
"""Set new operation mode."""
qos = self._config.get(CONF_QOS)
retain = self._config.get(CONF_RETAIN)
if self._topic[CONF_POWER_COMMAND_TOPIC] is not None:
if (self._current_operation == STATE_OFF and
operation_mode != STATE_OFF):
mqtt.async_publish(
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC],
self._payload_on, self._qos, self._retain)
self._config.get(CONF_PAYLOAD_ON), qos, retain)
elif (self._current_operation != STATE_OFF and
operation_mode == STATE_OFF):
mqtt.async_publish(
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC],
self._payload_off, self._qos, self._retain)
self._config.get(CONF_PAYLOAD_OFF), qos, retain)
if self._topic[CONF_MODE_COMMAND_TOPIC] is not None:
mqtt.async_publish(
self.hass, self._topic[CONF_MODE_COMMAND_TOPIC],
operation_mode, self._qos, self._retain)
operation_mode, qos, retain)
if self._topic[CONF_MODE_STATE_TOPIC] is None:
self._current_operation = operation_mode
@@ -568,14 +600,16 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
return self._config.get(CONF_SWING_MODE_LIST)
async def async_turn_away_mode_on(self):
"""Turn away mode on."""
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass,
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC],
self._payload_on, self._qos, self._retain)
self._config.get(CONF_PAYLOAD_ON),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
self._away = True
@@ -586,7 +620,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass,
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC],
self._payload_off, self._qos, self._retain)
self._config.get(CONF_PAYLOAD_OFF),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
self._away = False
@@ -597,7 +633,8 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass,
self._topic[CONF_HOLD_COMMAND_TOPIC],
hold_mode, self._qos, self._retain)
hold_mode, self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._topic[CONF_HOLD_STATE_TOPIC] is None:
self._hold = hold_mode
@@ -607,7 +644,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
"""Turn auxiliary heater on."""
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
self._payload_on, self._qos, self._retain)
self._config.get(CONF_PAYLOAD_ON),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._topic[CONF_AUX_STATE_TOPIC] is None:
self._aux = True
@@ -617,7 +656,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
"""Turn auxiliary heater off."""
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
self._payload_off, self._qos, self._retain)
self._config.get(CONF_PAYLOAD_OFF),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._topic[CONF_AUX_STATE_TOPIC] is None:
self._aux = False
@@ -661,9 +702,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._min_temp
return self._config.get(CONF_MIN_TEMP)
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._max_temp
return self._config.get(CONF_MAX_TEMP)

View File

@@ -15,6 +15,14 @@ from homeassistant.const import TEMP_CELSIUS
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
HA_TOON = {
STATE_AUTO: 'Comfort',
STATE_HEAT: 'Home',
STATE_ECO: 'Away',
STATE_COOL: 'Sleep',
}
TOON_HA = {value: key for key, value in HA_TOON.items()}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Toon climate device."""
@@ -58,8 +66,7 @@ class ThermostatDevice(ClimateDevice):
@property
def current_operation(self):
"""Return current operation i.e. comfort, home, away."""
state = self.thermos.get_data('state')
return state
return TOON_HA.get(self.thermos.get_data('state'))
@property
def operation_list(self):
@@ -83,14 +90,7 @@ class ThermostatDevice(ClimateDevice):
def set_operation_mode(self, operation_mode):
"""Set new operation mode."""
toonlib_values = {
STATE_AUTO: 'Comfort',
STATE_HEAT: 'Home',
STATE_ECO: 'Away',
STATE_COOL: 'Sleep',
}
self.thermos.set_state(toonlib_values[operation_mode])
self.thermos.set_state(HA_TOON[operation_mode])
def update(self):
"""Update local state."""

View File

@@ -20,7 +20,7 @@ from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c
from . import http_api, iot, auth_api, prefs
from . import http_api, iot, auth_api, prefs, cloudhooks
from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.6.1']
@@ -37,6 +37,7 @@ CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
CONF_CLOUDHOOK_CREATE_URL = 'cloudhook_create_url'
DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
@@ -78,6 +79,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
vol.Optional(CONF_SUBSCRIPTION_INFO_URL): str,
vol.Optional(CONF_CLOUDHOOK_CREATE_URL): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}),
@@ -113,7 +115,7 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None,
relayer=None, google_actions_sync_url=None,
subscription_info_url=None):
subscription_info_url=None, cloudhook_create_url=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
@@ -125,6 +127,7 @@ class Cloud:
self.access_token = None
self.refresh_token = None
self.iot = iot.CloudIoT(self)
self.cloudhooks = cloudhooks.Cloudhooks(self)
if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id
@@ -133,6 +136,7 @@ class Cloud:
self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url
self.subscription_info_url = subscription_info_url
self.cloudhook_create_url = cloudhook_create_url
else:
info = SERVERS[mode]
@@ -143,6 +147,7 @@ class Cloud:
self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url']
self.subscription_info_url = info['subscription_info_url']
self.cloudhook_create_url = info['cloudhook_create_url']
@property
def is_logged_in(self):
@@ -186,9 +191,9 @@ class Cloud:
self._gactions_config = ga_h.Config(
should_expose=should_expose,
allow_unlock=self.prefs.google_allow_unlock,
agent_user_id=self.claims['cognito:username'],
entity_config=conf.get(CONF_ENTITY_CONFIG),
allow_unlock=self.prefs.google_allow_unlock,
)
return self._gactions_config
@@ -247,8 +252,7 @@ class Cloud:
return json.loads(file.read())
info = await self.hass.async_add_job(load_config)
await self.prefs.async_initialize(not info)
await self.prefs.async_initialize()
if info is None:
return

View File

@@ -0,0 +1,42 @@
"""Cloud APIs."""
from functools import wraps
import logging
from . import auth_api
_LOGGER = logging.getLogger(__name__)
def _check_token(func):
"""Decorate a function to verify valid token."""
@wraps(func)
async def check_token(cloud, *args):
"""Validate token, then call func."""
await cloud.hass.async_add_executor_job(auth_api.check_token, cloud)
return await func(cloud, *args)
return check_token
def _log_response(func):
"""Decorate a function to log bad responses."""
@wraps(func)
async def log_response(*args):
"""Log response if it's bad."""
resp = await func(*args)
meth = _LOGGER.debug if resp.status < 400 else _LOGGER.warning
meth('Fetched %s (%s)', resp.url, resp.status)
return resp
return log_response
@_check_token
@_log_response
async def async_create_cloudhook(cloud):
"""Create a cloudhook."""
websession = cloud.hass.helpers.aiohttp_client.async_get_clientsession()
return await websession.post(
cloud.cloudhook_create_url, headers={
'authorization': cloud.id_token
})

View File

@@ -0,0 +1,66 @@
"""Manage cloud cloudhooks."""
import async_timeout
from . import cloud_api
class Cloudhooks:
"""Class to help manage cloudhooks."""
def __init__(self, cloud):
"""Initialize cloudhooks."""
self.cloud = cloud
self.cloud.iot.register_on_connect(self.async_publish_cloudhooks)
async def async_publish_cloudhooks(self):
"""Inform the Relayer of the cloudhooks that we support."""
cloudhooks = self.cloud.prefs.cloudhooks
await self.cloud.iot.async_send_message('webhook-register', {
'cloudhook_ids': [info['cloudhook_id'] for info
in cloudhooks.values()]
}, expect_answer=False)
async def async_create(self, webhook_id):
"""Create a cloud webhook."""
cloudhooks = self.cloud.prefs.cloudhooks
if webhook_id in cloudhooks:
raise ValueError('Hook is already enabled for the cloud.')
if not self.cloud.iot.connected:
raise ValueError("Cloud is not connected")
# Create cloud hook
with async_timeout.timeout(10):
resp = await cloud_api.async_create_cloudhook(self.cloud)
data = await resp.json()
cloudhook_id = data['cloudhook_id']
cloudhook_url = data['url']
# Store hook
cloudhooks = dict(cloudhooks)
hook = cloudhooks[webhook_id] = {
'webhook_id': webhook_id,
'cloudhook_id': cloudhook_id,
'cloudhook_url': cloudhook_url
}
await self.cloud.prefs.async_update(cloudhooks=cloudhooks)
await self.async_publish_cloudhooks()
return hook
async def async_delete(self, webhook_id):
"""Delete a cloud webhook."""
cloudhooks = self.cloud.prefs.cloudhooks
if webhook_id not in cloudhooks:
raise ValueError('Hook is not enabled for the cloud.')
# Remove hook
cloudhooks = dict(cloudhooks)
cloudhooks.pop(webhook_id)
await self.cloud.prefs.async_update(cloudhooks=cloudhooks)
await self.async_publish_cloudhooks()

View File

@@ -6,6 +6,7 @@ REQUEST_TIMEOUT = 10
PREF_ENABLE_ALEXA = 'alexa_enabled'
PREF_ENABLE_GOOGLE = 'google_enabled'
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
PREF_CLOUDHOOKS = 'cloudhooks'
SERVERS = {
'production': {
@@ -16,7 +17,8 @@ SERVERS = {
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
'amazonaws.com/prod/smart_home_sync'),
'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/'
'subscription_info')
'subscription_info'),
'cloudhook_create_url': 'https://webhooks-api.nabucasa.com/generate'
}
}

View File

@@ -3,6 +3,7 @@ import asyncio
from functools import wraps
import logging
import aiohttp
import async_timeout
import voluptuous as vol
@@ -44,6 +45,20 @@ SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
})
WS_TYPE_HOOK_CREATE = 'cloud/cloudhook/create'
SCHEMA_WS_HOOK_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_HOOK_CREATE,
vol.Required('webhook_id'): str
})
WS_TYPE_HOOK_DELETE = 'cloud/cloudhook/delete'
SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_HOOK_DELETE,
vol.Required('webhook_id'): str
})
async def async_setup(hass):
"""Initialize the HTTP API."""
hass.components.websocket_api.async_register_command(
@@ -58,6 +73,14 @@ async def async_setup(hass):
WS_TYPE_UPDATE_PREFS, websocket_update_prefs,
SCHEMA_WS_UPDATE_PREFS
)
hass.components.websocket_api.async_register_command(
WS_TYPE_HOOK_CREATE, websocket_hook_create,
SCHEMA_WS_HOOK_CREATE
)
hass.components.websocket_api.async_register_command(
WS_TYPE_HOOK_DELETE, websocket_hook_delete,
SCHEMA_WS_HOOK_DELETE
)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
@@ -76,7 +99,7 @@ _CLOUD_ERRORS = {
def _handle_cloud_errors(handler):
"""Handle auth errors."""
"""Webview decorator to handle auth errors."""
@wraps(handler)
async def error_handler(view, request, *args, **kwargs):
"""Handle exceptions that raise from the wrapped request handler."""
@@ -240,17 +263,49 @@ def websocket_cloud_status(hass, connection, msg):
websocket_api.result_message(msg['id'], _account_data(cloud)))
def _require_cloud_login(handler):
"""Websocket decorator that requires cloud to be logged in."""
@wraps(handler)
def with_cloud_auth(hass, connection, msg):
"""Require to be logged into the cloud."""
cloud = hass.data[DOMAIN]
if not cloud.is_logged_in:
connection.send_message(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return
handler(hass, connection, msg)
return with_cloud_auth
def _handle_aiohttp_errors(handler):
"""Websocket decorator that handlers aiohttp errors.
Can only wrap async handlers.
"""
@wraps(handler)
async def with_error_handling(hass, connection, msg):
"""Handle aiohttp errors."""
try:
await handler(hass, connection, msg)
except asyncio.TimeoutError:
connection.send_message(websocket_api.error_message(
msg['id'], 'timeout', 'Command timed out.'))
except aiohttp.ClientError:
connection.send_message(websocket_api.error_message(
msg['id'], 'unknown', 'Error making request.'))
return with_error_handling
@_require_cloud_login
@websocket_api.async_response
async def websocket_subscription(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
if not cloud.is_logged_in:
connection.send_message(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
response = await cloud.fetch_subscription_info()
@@ -277,24 +332,37 @@ async def websocket_subscription(hass, connection, msg):
connection.send_message(websocket_api.result_message(msg['id'], data))
@_require_cloud_login
@websocket_api.async_response
async def websocket_update_prefs(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
if not cloud.is_logged_in:
connection.send_message(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return
changes = dict(msg)
changes.pop('id')
changes.pop('type')
await cloud.prefs.async_update(**changes)
connection.send_message(websocket_api.result_message(
msg['id'], {'success': True}))
connection.send_message(websocket_api.result_message(msg['id']))
@_require_cloud_login
@websocket_api.async_response
@_handle_aiohttp_errors
async def websocket_hook_create(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
hook = await cloud.cloudhooks.async_create(msg['webhook_id'])
connection.send_message(websocket_api.result_message(msg['id'], hook))
@_require_cloud_login
@websocket_api.async_response
async def websocket_hook_delete(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
await cloud.cloudhooks.async_delete(msg['webhook_id'])
connection.send_message(websocket_api.result_message(msg['id']))
def _account_data(cloud):

View File

@@ -2,13 +2,16 @@
import asyncio
import logging
import pprint
import uuid
from aiohttp import hdrs, client_exceptions, WSMsgType
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.alexa import smart_home as alexa
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.core import callback
from homeassistant.util.decorator import Registry
from homeassistant.util.aiohttp import MockRequest, serialize_response
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api
from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL
@@ -25,6 +28,19 @@ class UnknownHandler(Exception):
"""Exception raised when trying to handle unknown handler."""
class NotConnected(Exception):
"""Exception raised when trying to handle unknown handler."""
class ErrorMessage(Exception):
"""Exception raised when there was error handling message in the cloud."""
def __init__(self, error):
"""Initialize Error Message."""
super().__init__(self, "Error in Cloud")
self.error = error
class CloudIoT:
"""Class to manage the IoT connection."""
@@ -41,6 +57,19 @@ class CloudIoT:
self.tries = 0
# Current state of the connection
self.state = STATE_DISCONNECTED
# Local code waiting for a response
self._response_handler = {}
self._on_connect = []
@callback
def register_on_connect(self, on_connect_cb):
"""Register an async on_connect callback."""
self._on_connect.append(on_connect_cb)
@property
def connected(self):
"""Return if we're currently connected."""
return self.state == STATE_CONNECTED
@asyncio.coroutine
def connect(self):
@@ -91,6 +120,30 @@ class CloudIoT:
if remove_hass_stop_listener is not None:
remove_hass_stop_listener()
async def async_send_message(self, handler, payload,
expect_answer=True):
"""Send a message."""
if self.state != STATE_CONNECTED:
raise NotConnected
msgid = uuid.uuid4().hex
if expect_answer:
fut = self._response_handler[msgid] = asyncio.Future()
message = {
'msgid': msgid,
'handler': handler,
'payload': payload,
}
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Publishing message:\n%s\n",
pprint.pformat(message))
await self.client.send_json(message)
if expect_answer:
return await fut
@asyncio.coroutine
def _handle_connection(self):
"""Connect to the IoT broker."""
@@ -134,6 +187,9 @@ class CloudIoT:
_LOGGER.info("Connected")
self.state = STATE_CONNECTED
if self._on_connect:
yield from asyncio.wait([cb() for cb in self._on_connect])
while not client.closed:
msg = yield from client.receive()
@@ -159,6 +215,17 @@ class CloudIoT:
_LOGGER.debug("Received message:\n%s\n",
pprint.pformat(msg))
response_handler = self._response_handler.pop(msg['msgid'],
None)
if response_handler is not None:
if 'payload' in msg:
response_handler.set_result(msg["payload"])
else:
response_handler.set_exception(
ErrorMessage(msg['error']))
continue
response = {
'msgid': msg['msgid'],
}
@@ -257,3 +324,43 @@ def async_handle_cloud(hass, cloud, payload):
payload['reason'])
else:
_LOGGER.warning("Received unknown cloud action: %s", action)
@HANDLERS.register('webhook')
async def async_handle_webhook(hass, cloud, payload):
"""Handle an incoming IoT message for cloud webhooks."""
cloudhook_id = payload['cloudhook_id']
found = None
for cloudhook in cloud.prefs.cloudhooks.values():
if cloudhook['cloudhook_id'] == cloudhook_id:
found = cloudhook
break
if found is None:
return {
'status': 200
}
request = MockRequest(
content=payload['body'].encode('utf-8'),
headers=payload['headers'],
method=payload['method'],
query_string=payload['query'],
)
response = await hass.components.webhook.async_handle_webhook(
found['webhook_id'], request)
response_dict = serialize_response(response)
body = response_dict.get('body')
if body:
body = body.decode('utf-8')
return {
'body': body,
'status': response_dict['status'],
'headers': {
'Content-Type': response.content_type
}
}

View File

@@ -1,7 +1,7 @@
"""Preference management for cloud."""
from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
PREF_GOOGLE_ALLOW_UNLOCK)
PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS)
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
@@ -16,27 +16,29 @@ class CloudPreferences:
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._prefs = None
async def async_initialize(self, logged_in):
async def async_initialize(self):
"""Finish initializing the preferences."""
prefs = await self._store.async_load()
if prefs is None:
# Backwards compat: we enable alexa/google if already logged in
prefs = {
PREF_ENABLE_ALEXA: logged_in,
PREF_ENABLE_GOOGLE: logged_in,
PREF_ENABLE_ALEXA: True,
PREF_ENABLE_GOOGLE: True,
PREF_GOOGLE_ALLOW_UNLOCK: False,
PREF_CLOUDHOOKS: {}
}
self._prefs = prefs
async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF):
alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF,
cloudhooks=_UNDEF):
"""Update user preferences."""
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_ALEXA, alexa_enabled),
(PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock),
(PREF_CLOUDHOOKS, cloudhooks),
):
if value is not _UNDEF:
self._prefs[key] = value
@@ -61,3 +63,8 @@ class CloudPreferences:
def google_allow_unlock(self):
"""Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False)
@property
def cloudhooks(self):
"""Return the published cloud webhooks."""
return self._prefs.get(PREF_CLOUDHOOKS, {})

View File

@@ -14,6 +14,8 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = (
'auth',
'auth_provider_homeassistant',
'automation',
'config_entries',
'core',
@@ -58,10 +60,6 @@ async def async_setup(hass, config):
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
if hass.auth.active:
tasks.append(setup_panel('auth'))
tasks.append(setup_panel('auth_provider_homeassistant'))
for panel_name in ON_DEMAND:
if panel_name in hass.config.components:
tasks.append(setup_panel(panel_name))

View File

@@ -10,9 +10,8 @@ import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.restore_state import RestoreEntity
_LOGGER = logging.getLogger(__name__)
@@ -86,7 +85,7 @@ async def async_setup(hass, config):
return True
class Counter(Entity):
class Counter(RestoreEntity):
"""Representation of a counter."""
def __init__(self, object_id, name, initial, restore, step, icon):
@@ -128,10 +127,11 @@ class Counter(Entity):
async def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant."""
await super().async_added_to_hass()
# __init__ will set self._state to self._initial, only override
# if needed.
if self._restore:
state = await async_get_last_state(self.hass, self.entity_id)
state = await self.async_get_last_state()
if state is not None:
self._state = int(state.state)

View File

@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.mqtt/
"""
import logging
from typing import Optional
import voluptuous as vol
@@ -24,7 +23,7 @@ from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic,
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -130,7 +129,7 @@ PLATFORM_SCHEMA = vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None):
"""Set up MQTT cover through configuration.yaml."""
await _async_setup_entity(hass, config, async_add_entities)
await _async_setup_entity(config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -138,7 +137,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_discover(discovery_payload):
"""Discover and add an MQTT cover."""
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(hass, config, async_add_entities,
await _async_setup_entity(config, async_add_entities,
discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect(
@@ -146,112 +145,78 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_discover)
async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None):
async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
"""Set up the MQTT Cover."""
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = hass
set_position_template = config.get(CONF_SET_POSITION_TEMPLATE)
if set_position_template is not None:
set_position_template.hass = hass
async_add_entities([MqttCover(
config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC),
config.get(CONF_GET_POSITION_TOPIC),
config.get(CONF_COMMAND_TOPIC),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_TILT_COMMAND_TOPIC),
config.get(CONF_TILT_STATUS_TOPIC),
config.get(CONF_QOS),
config.get(CONF_RETAIN),
config.get(CONF_STATE_OPEN),
config.get(CONF_STATE_CLOSED),
config.get(CONF_POSITION_OPEN),
config.get(CONF_POSITION_CLOSED),
config.get(CONF_PAYLOAD_OPEN),
config.get(CONF_PAYLOAD_CLOSE),
config.get(CONF_PAYLOAD_STOP),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_OPTIMISTIC),
value_template,
config.get(CONF_TILT_OPEN_POSITION),
config.get(CONF_TILT_CLOSED_POSITION),
config.get(CONF_TILT_MIN),
config.get(CONF_TILT_MAX),
config.get(CONF_TILT_STATE_OPTIMISTIC),
config.get(CONF_TILT_INVERT_STATE),
config.get(CONF_SET_POSITION_TOPIC),
set_position_template,
config.get(CONF_UNIQUE_ID),
config.get(CONF_DEVICE),
discovery_hash
)])
async_add_entities([MqttCover(config, discovery_hash)])
class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
CoverDevice):
"""Representation of a cover that can be controlled using MQTT."""
def __init__(self, name, state_topic, get_position_topic,
command_topic, availability_topic,
tilt_command_topic, tilt_status_topic, qos, retain,
state_open, state_closed, position_open, position_closed,
payload_open, payload_close, payload_stop, payload_available,
payload_not_available, optimistic, value_template,
tilt_open_position, tilt_closed_position, tilt_min, tilt_max,
tilt_optimistic, tilt_invert, set_position_topic,
set_position_template, unique_id: Optional[str],
device_config: Optional[ConfigType], discovery_hash):
def __init__(self, config, discovery_hash):
"""Initialize the cover."""
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
MqttEntityDeviceInfo.__init__(self, device_config)
self._unique_id = config.get(CONF_UNIQUE_ID)
self._position = None
self._state = None
self._name = name
self._state_topic = state_topic
self._get_position_topic = get_position_topic
self._command_topic = command_topic
self._tilt_command_topic = tilt_command_topic
self._tilt_status_topic = tilt_status_topic
self._qos = qos
self._payload_open = payload_open
self._payload_close = payload_close
self._payload_stop = payload_stop
self._state_open = state_open
self._state_closed = state_closed
self._position_open = position_open
self._position_closed = position_closed
self._retain = retain
self._tilt_open_position = tilt_open_position
self._tilt_closed_position = tilt_closed_position
self._optimistic = (optimistic or (state_topic is None and
get_position_topic is None))
self._template = value_template
self._sub_state = None
self._optimistic = None
self._tilt_value = None
self._tilt_min = tilt_min
self._tilt_max = tilt_max
self._tilt_optimistic = tilt_optimistic
self._tilt_invert = tilt_invert
self._set_position_topic = set_position_topic
self._set_position_template = set_position_template
self._unique_id = unique_id
self._discovery_hash = discovery_hash
self._tilt_optimistic = None
# Load config
self._setup_from_config(config)
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE)
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update)
MqttEntityDeviceInfo.__init__(self, device_config)
async def async_added_to_hass(self):
"""Subscribe MQTT events."""
await MqttAvailability.async_added_to_hass(self)
await MqttDiscoveryUpdate.async_added_to_hass(self)
await super().async_added_to_hass()
await self._subscribe_topics()
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._setup_from_config(config)
await self.availability_discovery_update(config)
await self._subscribe_topics()
self.async_schedule_update_ha_state()
def _setup_from_config(self, config):
self._config = config
self._optimistic = (config.get(CONF_OPTIMISTIC) or
(config.get(CONF_STATE_TOPIC) is None and
config.get(CONF_GET_POSITION_TOPIC) is None))
self._tilt_optimistic = config.get(CONF_TILT_STATE_OPTIMISTIC)
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
template.hass = self.hass
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
if set_position_template is not None:
set_position_template.hass = self.hass
topics = {}
@callback
def tilt_updated(topic, payload, qos):
"""Handle tilt updates."""
if (payload.isnumeric() and
self._tilt_min <= int(payload) <= self._tilt_max):
(self._config.get(CONF_TILT_MIN) <= int(payload) <=
self._config.get(CONF_TILT_MAX))):
level = self.find_percentage_in_range(float(payload))
self._tilt_value = level
@@ -260,13 +225,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
@callback
def state_message_received(topic, payload, qos):
"""Handle new MQTT state messages."""
if self._template is not None:
payload = self._template.async_render_with_possible_json_value(
if template is not None:
payload = template.async_render_with_possible_json_value(
payload)
if payload == self._state_open:
if payload == self._config.get(CONF_STATE_OPEN):
self._state = False
elif payload == self._state_closed:
elif payload == self._config.get(CONF_STATE_CLOSED):
self._state = True
else:
_LOGGER.warning("Payload is not True or False: %s", payload)
@@ -276,8 +241,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
@callback
def position_message_received(topic, payload, qos):
"""Handle new MQTT state messages."""
if self._template is not None:
payload = self._template.async_render_with_possible_json_value(
if template is not None:
payload = template.async_render_with_possible_json_value(
payload)
if payload.isnumeric():
@@ -292,25 +257,38 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
return
self.async_schedule_update_ha_state()
if self._get_position_topic:
await mqtt.async_subscribe(
self.hass, self._get_position_topic,
position_message_received, self._qos)
elif self._state_topic:
await mqtt.async_subscribe(
self.hass, self._state_topic,
state_message_received, self._qos)
if self._config.get(CONF_GET_POSITION_TOPIC):
topics['get_position_topic'] = {
'topic': self._config.get(CONF_GET_POSITION_TOPIC),
'msg_callback': position_message_received,
'qos': self._config.get(CONF_QOS)}
elif self._config.get(CONF_STATE_TOPIC):
topics['state_topic'] = {
'topic': self._config.get(CONF_STATE_TOPIC),
'msg_callback': state_message_received,
'qos': self._config.get(CONF_QOS)}
else:
# Force into optimistic mode.
self._optimistic = True
if self._tilt_status_topic is None:
if self._config.get(CONF_TILT_STATUS_TOPIC) is None:
self._tilt_optimistic = True
else:
self._tilt_optimistic = False
self._tilt_value = STATE_UNKNOWN
await mqtt.async_subscribe(
self.hass, self._tilt_status_topic, tilt_updated, self._qos)
topics['tilt_status_topic'] = {
'topic': self._config.get(CONF_TILT_STATUS_TOPIC),
'msg_callback': tilt_updated,
'qos': self._config.get(CONF_QOS)}
self._sub_state = await subscription.async_subscribe_topics(
self.hass, self._sub_state,
topics)
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
await MqttAvailability.async_will_remove_from_hass(self)
@property
def should_poll(self):
@@ -325,7 +303,7 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
@property
def name(self):
"""Return the name of the cover."""
return self._name
return self._config.get(CONF_NAME)
@property
def is_closed(self):
@@ -349,13 +327,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
def supported_features(self):
"""Flag supported features."""
supported_features = 0
if self._command_topic is not None:
if self._config.get(CONF_COMMAND_TOPIC) is not None:
supported_features = OPEN_CLOSE_FEATURES
if self._set_position_topic is not None:
if self._config.get(CONF_SET_POSITION_TOPIC) is not None:
supported_features |= SUPPORT_SET_POSITION
if self._tilt_command_topic is not None:
if self._config.get(CONF_TILT_COMMAND_TOPIC) is not None:
supported_features |= TILT_FEATURES
return supported_features
@@ -366,14 +344,15 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
This method is a coroutine.
"""
mqtt.async_publish(
self.hass, self._command_topic, self._payload_open, self._qos,
self._retain)
self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._config.get(CONF_PAYLOAD_OPEN), self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._optimistic:
# Optimistically assume that cover has changed state.
self._state = False
if self._get_position_topic:
if self._config.get(CONF_GET_POSITION_TOPIC):
self._position = self.find_percentage_in_range(
self._position_open, COVER_PAYLOAD)
self._config.get(CONF_POSITION_OPEN), COVER_PAYLOAD)
self.async_schedule_update_ha_state()
async def async_close_cover(self, **kwargs):
@@ -382,14 +361,15 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
This method is a coroutine.
"""
mqtt.async_publish(
self.hass, self._command_topic, self._payload_close, self._qos,
self._retain)
self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._config.get(CONF_PAYLOAD_CLOSE), self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._optimistic:
# Optimistically assume that cover has changed state.
self._state = True
if self._get_position_topic:
if self._config.get(CONF_GET_POSITION_TOPIC):
self._position = self.find_percentage_in_range(
self._position_closed, COVER_PAYLOAD)
self._config.get(CONF_POSITION_CLOSED), COVER_PAYLOAD)
self.async_schedule_update_ha_state()
async def async_stop_cover(self, **kwargs):
@@ -398,25 +378,30 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
This method is a coroutine.
"""
mqtt.async_publish(
self.hass, self._command_topic, self._payload_stop, self._qos,
self._retain)
self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._config.get(CONF_PAYLOAD_STOP), self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
async def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open."""
mqtt.async_publish(self.hass, self._tilt_command_topic,
self._tilt_open_position, self._qos,
self._retain)
mqtt.async_publish(self.hass,
self._config.get(CONF_TILT_COMMAND_TOPIC),
self._config.get(CONF_TILT_OPEN_POSITION),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._tilt_optimistic:
self._tilt_value = self._tilt_open_position
self._tilt_value = self._config.get(CONF_TILT_OPEN_POSITION)
self.async_schedule_update_ha_state()
async def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed."""
mqtt.async_publish(self.hass, self._tilt_command_topic,
self._tilt_closed_position, self._qos,
self._retain)
mqtt.async_publish(self.hass,
self._config.get(CONF_TILT_COMMAND_TOPIC),
self._config.get(CONF_TILT_CLOSED_POSITION),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._tilt_optimistic:
self._tilt_value = self._tilt_closed_position
self._tilt_value = self._config.get(CONF_TILT_CLOSED_POSITION)
self.async_schedule_update_ha_state()
async def async_set_cover_tilt_position(self, **kwargs):
@@ -429,29 +414,38 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
# The position needs to be between min and max
level = self.find_in_range_from_percent(position)
mqtt.async_publish(self.hass, self._tilt_command_topic,
level, self._qos, self._retain)
mqtt.async_publish(self.hass,
self._config.get(CONF_TILT_COMMAND_TOPIC),
level,
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
percentage_position = position
if self._set_position_template is not None:
if set_position_template is not None:
try:
position = self._set_position_template.async_render(
position = set_position_template.async_render(
**kwargs)
except TemplateError as ex:
_LOGGER.error(ex)
self._state = None
elif self._position_open != 100 and self._position_closed != 0:
elif (self._config.get(CONF_POSITION_OPEN) != 100 and
self._config.get(CONF_POSITION_CLOSED) != 0):
position = self.find_in_range_from_percent(
position, COVER_PAYLOAD)
mqtt.async_publish(self.hass, self._set_position_topic,
position, self._qos, self._retain)
mqtt.async_publish(self.hass,
self._config.get(CONF_SET_POSITION_TOPIC),
position,
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._optimistic:
self._state = percentage_position == self._position_closed
self._state = percentage_position == \
self._config.get(CONF_POSITION_CLOSED)
self._position = percentage_position
self.async_schedule_update_ha_state()
@@ -459,11 +453,11 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
"""Find the 0-100% value within the specified range."""
# the range of motion as defined by the min max values
if range_type == COVER_PAYLOAD:
max_range = self._position_open
min_range = self._position_closed
max_range = self._config.get(CONF_POSITION_OPEN)
min_range = self._config.get(CONF_POSITION_CLOSED)
else:
max_range = self._tilt_max
min_range = self._tilt_min
max_range = self._config.get(CONF_TILT_MAX)
min_range = self._config.get(CONF_TILT_MIN)
current_range = max_range - min_range
# offset to be zero based
offset_position = position - min_range
@@ -474,7 +468,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
min_percent = 0
position_percentage = min(max(position_percentage, min_percent),
max_percent)
if range_type == TILT_PAYLOAD and self._tilt_invert:
if range_type == TILT_PAYLOAD and \
self._config.get(CONF_TILT_INVERT_STATE):
return 100 - position_percentage
return position_percentage
@@ -488,17 +483,18 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
returning the offset
"""
if range_type == COVER_PAYLOAD:
max_range = self._position_open
min_range = self._position_closed
max_range = self._config.get(CONF_POSITION_OPEN)
min_range = self._config.get(CONF_POSITION_CLOSED)
else:
max_range = self._tilt_max
min_range = self._tilt_min
max_range = self._config.get(CONF_TILT_MAX)
min_range = self._config.get(CONF_TILT_MIN)
offset = min_range
current_range = max_range - min_range
position = round(current_range * (percentage / 100.0))
position += offset
if range_type == TILT_PAYLOAD and self._tilt_invert:
if range_type == TILT_PAYLOAD and \
self._config.get(CONF_TILT_INVERT_STATE):
position = max_range - position + offset
return position

View File

@@ -8,8 +8,9 @@ https://home-assistant.io/components/cover.tellduslive/
"""
import logging
from homeassistant.components import tellduslive
from homeassistant.components.cover import CoverDevice
from homeassistant.components.tellduslive import TelldusLiveEntity
from homeassistant.components.tellduslive.entry import TelldusLiveEntity
_LOGGER = logging.getLogger(__name__)
@@ -19,7 +20,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if discovery_info is None:
return
add_entities(TelldusLiveCover(hass, cover) for cover in discovery_info)
client = hass.data[tellduslive.DOMAIN]
add_entities(TelldusLiveCover(client, cover) for cover in discovery_info)
class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
@@ -33,14 +35,11 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
def close_cover(self, **kwargs):
"""Close the cover."""
self.device.down()
self.changed()
def open_cover(self, **kwargs):
"""Open the cover."""
self.device.up()
self.changed()
def stop_cover(self, **kwargs):
"""Stop the cover."""
self.device.stop()
self.changed()

View File

@@ -132,3 +132,8 @@ class DaikinApi:
_LOGGER.warning(
"Connection failed for %s", self.ip_address
)
@property
def mac(self):
"""Return mac-address of device."""
return self.device.values.get('mac')

View File

@@ -12,7 +12,7 @@
"init": {
"data": {
"host": "Amfitri\u00f3",
"port": "Port (predeterminat: '80')"
"port": "Port"
},
"title": "Definiu la passarel\u00b7la deCONZ"
},

View File

@@ -17,7 +17,7 @@
"title": "deCONZ gateway d\u00e9fin\u00e9ieren"
},
"link": {
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
"title": "Link mat deCONZ"
},
"options": {

View File

@@ -12,7 +12,7 @@
"init": {
"data": {
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: '80')"
"port": "\u041f\u043e\u0440\u0442"
},
"title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ"
},

View File

@@ -22,9 +22,8 @@ from homeassistant.components.zone.zone import async_active_zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv
from homeassistant import util
@@ -182,6 +181,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
setup = await hass.async_add_job(
platform.setup_scanner, hass, p_config, tracker.see,
disc_info)
elif hasattr(platform, 'async_setup_entry'):
setup = await platform.async_setup_entry(
hass, p_config, tracker.async_see)
else:
raise HomeAssistantError("Invalid device_tracker platform.")
@@ -197,6 +199,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error setting up platform %s", p_type)
hass.data[DOMAIN] = async_setup_platform
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
in config_per_platform(config, DOMAIN)]
if setup_tasks:
@@ -230,6 +234,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
return True
async def async_setup_entry(hass, entry):
"""Set up an entry."""
await hass.data[DOMAIN](entry.domain, entry)
return True
class DeviceTracker:
"""Representation of a device tracker."""
@@ -373,7 +383,6 @@ class DeviceTracker:
for device in self.devices.values():
if (device.track and device.last_update_home) and \
device.stale(now):
device.mark_stale()
self.hass.async_create_task(device.async_update_ha_state(True))
async def async_setup_tracked_device(self):
@@ -396,7 +405,7 @@ class DeviceTracker:
await asyncio.wait(tasks, loop=self.hass.loop)
class Device(Entity):
class Device(RestoreEntity):
"""Represent a tracked device."""
host_name = None # type: str
@@ -564,7 +573,8 @@ class Device(Entity):
async def async_added_to_hass(self):
"""Add an entity."""
state = await async_get_last_state(self.hass, self.entity_id)
await super().async_added_to_hass()
state = await self.async_get_last_state()
if not state:
return
self._state = state.state

View File

@@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['btsmarthub_devicelist==0.1.1']
REQUIREMENTS = ['btsmarthub_devicelist==0.1.3']
_LOGGER = logging.getLogger(__name__)

View File

@@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify, dt as dt_util
REQUIREMENTS = ['locationsharinglib==3.0.8']
REQUIREMENTS = ['locationsharinglib==3.0.9']
_LOGGER = logging.getLogger(__name__)

View File

@@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['ghlocalapi==0.1.0']
REQUIREMENTS = ['ghlocalapi==0.3.5']
_LOGGER = logging.getLogger(__name__)
@@ -89,4 +89,5 @@ class GoogleHomeDeviceScanner(DeviceScanner):
devices[uuid]['btle_mac_address'] = device['mac_address']
devices[uuid]['ghname'] = ghname
devices[uuid]['source_type'] = 'bluetooth'
await self.scanner.clear_scan_result()
self.last_results = devices

View File

@@ -16,7 +16,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_METHOD)
REQUIREMENTS = ['librouteros==2.1.1']
REQUIREMENTS = ['librouteros==2.2.0']
_LOGGER = logging.getLogger(__name__)
@@ -128,7 +128,8 @@ class MikrotikScanner(DeviceScanner):
librouteros.exceptions.ConnectionError):
self.wireless_exist = False
if not self.wireless_exist or self.method == 'ip':
if not self.wireless_exist and not self.capsman_exist \
or self.method == 'ip':
_LOGGER.info(
"Mikrotik %s: Wireless adapters not found. Try to "
"use DHCP lease table as presence tracker source. "
@@ -143,12 +144,18 @@ class MikrotikScanner(DeviceScanner):
librouteros.exceptions.MultiTrapError,
librouteros.exceptions.ConnectionError) as api_error:
_LOGGER.error("Connection error: %s", api_error)
return self.connected
def scan_devices(self):
"""Scan for new devices and return a list with found device MACs."""
self._update_info()
import librouteros
try:
self._update_info()
except (librouteros.exceptions.TrapError,
librouteros.exceptions.MultiTrapError,
librouteros.exceptions.ConnectionError) as api_error:
_LOGGER.error("Connection error: %s", api_error)
self.connect_to_device()
return [device for device in self.last_results]
def get_device_name(self, device):

View File

@@ -7,55 +7,29 @@ https://home-assistant.io/components/device_tracker.owntracks/
import base64
import json
import logging
from collections import defaultdict
import voluptuous as vol
from homeassistant.components import mqtt
import homeassistant.helpers.config_validation as cv
from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE,
SOURCE_TYPE_GPS
ATTR_SOURCE_TYPE, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS
)
from homeassistant.components.owntracks import DOMAIN as OT_DOMAIN
from homeassistant.const import STATE_HOME
from homeassistant.core import callback
from homeassistant.util import slugify, decorator
REQUIREMENTS = ['libnacl==1.6.1']
DEPENDENCIES = ['owntracks']
_LOGGER = logging.getLogger(__name__)
HANDLERS = decorator.Registry()
BEACON_DEV_ID = 'beacon'
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
CONF_SECRET = 'secret'
CONF_WAYPOINT_IMPORT = 'waypoints'
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
CONF_MQTT_TOPIC = 'mqtt_topic'
CONF_REGION_MAPPING = 'region_mapping'
CONF_EVENTS_ONLY = 'events_only'
DEPENDENCIES = ['mqtt']
DEFAULT_OWNTRACKS_TOPIC = 'owntracks/#'
REGION_MAPPING = {}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
vol.Optional(CONF_EVENTS_ONLY, default=False): cv.boolean,
vol.Optional(CONF_MQTT_TOPIC, default=DEFAULT_OWNTRACKS_TOPIC):
mqtt.valid_subscribe_topic,
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
cv.ensure_list, [cv.string]),
vol.Optional(CONF_SECRET): vol.Any(
vol.Schema({vol.Optional(cv.string): cv.string}),
cv.string),
vol.Optional(CONF_REGION_MAPPING, default=REGION_MAPPING): dict
})
async def async_setup_entry(hass, entry, async_see):
"""Set up OwnTracks based off an entry."""
hass.data[OT_DOMAIN]['context'].async_see = async_see
hass.helpers.dispatcher.async_dispatcher_connect(
OT_DOMAIN, async_handle_message)
return True
def get_cipher():
@@ -72,29 +46,6 @@ def get_cipher():
return (KEYLEN, decrypt)
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an OwnTracks tracker."""
context = context_from_config(async_see, config)
async def async_handle_mqtt_message(topic, payload, qos):
"""Handle incoming OwnTracks message."""
try:
message = json.loads(payload)
except ValueError:
# If invalid JSON
_LOGGER.error("Unable to parse payload as JSON: %s", payload)
return
message['topic'] = topic
await async_handle_message(hass, context, message)
await mqtt.async_subscribe(
hass, context.mqtt_topic, async_handle_mqtt_message, 1)
return True
def _parse_topic(topic, subscribe_topic):
"""Parse an MQTT topic {sub_topic}/user/dev, return (user, dev) tuple.
@@ -202,93 +153,6 @@ def _decrypt_payload(secret, topic, ciphertext):
return None
def context_from_config(async_see, config):
"""Create an async context from Home Assistant config."""
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
secret = config.get(CONF_SECRET)
region_mapping = config.get(CONF_REGION_MAPPING)
events_only = config.get(CONF_EVENTS_ONLY)
mqtt_topic = config.get(CONF_MQTT_TOPIC)
return OwnTracksContext(async_see, secret, max_gps_accuracy,
waypoint_import, waypoint_whitelist,
region_mapping, events_only, mqtt_topic)
class OwnTracksContext:
"""Hold the current OwnTracks context."""
def __init__(self, async_see, secret, max_gps_accuracy, import_waypoints,
waypoint_whitelist, region_mapping, events_only, mqtt_topic):
"""Initialize an OwnTracks context."""
self.async_see = async_see
self.secret = secret
self.max_gps_accuracy = max_gps_accuracy
self.mobile_beacons_active = defaultdict(set)
self.regions_entered = defaultdict(list)
self.import_waypoints = import_waypoints
self.waypoint_whitelist = waypoint_whitelist
self.region_mapping = region_mapping
self.events_only = events_only
self.mqtt_topic = mqtt_topic
@callback
def async_valid_accuracy(self, message):
"""Check if we should ignore this message."""
acc = message.get('acc')
if acc is None:
return False
try:
acc = float(acc)
except ValueError:
return False
if acc == 0:
_LOGGER.warning(
"Ignoring %s update because GPS accuracy is zero: %s",
message['_type'], message)
return False
if self.max_gps_accuracy is not None and \
acc > self.max_gps_accuracy:
_LOGGER.info("Ignoring %s update because expected GPS "
"accuracy %s is not met: %s",
message['_type'], self.max_gps_accuracy,
message)
return False
return True
async def async_see_beacons(self, hass, dev_id, kwargs_param):
"""Set active beacons to the current location."""
kwargs = kwargs_param.copy()
# Mobile beacons should always be set to the location of the
# tracking device. I get the device state and make the necessary
# changes to kwargs.
device_tracker_state = hass.states.get(
"device_tracker.{}".format(dev_id))
if device_tracker_state is not None:
acc = device_tracker_state.attributes.get("gps_accuracy")
lat = device_tracker_state.attributes.get("latitude")
lon = device_tracker_state.attributes.get("longitude")
kwargs['gps_accuracy'] = acc
kwargs['gps'] = (lat, lon)
# the battery state applies to the tracking device, not the beacon
# kwargs location is the beacon's configured lat/lon
kwargs.pop('battery', None)
for beacon in self.mobile_beacons_active[dev_id]:
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
kwargs['host_name'] = beacon
await self.async_see(**kwargs)
@HANDLERS.register('location')
async def async_handle_location_message(hass, context, message):
"""Handle a location message."""
@@ -452,14 +316,19 @@ async def async_handle_waypoints_message(hass, context, message):
@HANDLERS.register('encrypted')
async def async_handle_encrypted_message(hass, context, message):
"""Handle an encrypted message."""
plaintext_payload = _decrypt_payload(context.secret, message['topic'],
if 'topic' not in message and isinstance(context.secret, dict):
_LOGGER.error("You cannot set per topic secrets when using HTTP")
return
plaintext_payload = _decrypt_payload(context.secret, message.get('topic'),
message['data'])
if plaintext_payload is None:
return
decrypted = json.loads(plaintext_payload)
decrypted['topic'] = message['topic']
if 'topic' in message and 'topic' not in decrypted:
decrypted['topic'] = message['topic']
await async_handle_message(hass, context, decrypted)
@@ -485,6 +354,8 @@ async def async_handle_message(hass, context, message):
"""Handle an OwnTracks message."""
msgtype = message.get('_type')
_LOGGER.debug("Received %s", message)
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
await handler(hass, context, message)

View File

@@ -1,82 +0,0 @@
"""
Device tracker platform that adds support for OwnTracks over HTTP.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks_http/
"""
import json
import logging
import re
from aiohttp.web import Response
import voluptuous as vol
# pylint: disable=unused-import
from homeassistant.components.device_tracker.owntracks import ( # NOQA
PLATFORM_SCHEMA, REQUIREMENTS, async_handle_message, context_from_config)
from homeassistant.const import CONF_WEBHOOK_ID
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['webhook']
_LOGGER = logging.getLogger(__name__)
EVENT_RECEIVED = 'owntracks_http_webhook_received'
EVENT_RESPONSE = 'owntracks_http_webhook_response_'
DOMAIN = 'device_tracker.owntracks_http'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_WEBHOOK_ID): cv.string
})
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up OwnTracks HTTP component."""
context = context_from_config(async_see, config)
subscription = context.mqtt_topic
topic = re.sub('/#$', '', subscription)
async def handle_webhook(hass, webhook_id, request):
"""Handle webhook callback."""
headers = request.headers
data = dict()
if 'X-Limit-U' in headers:
data['user'] = headers['X-Limit-U']
elif 'u' in request.query:
data['user'] = request.query['u']
else:
return Response(
body=json.dumps({'error': 'You need to supply username.'}),
content_type="application/json"
)
if 'X-Limit-D' in headers:
data['device'] = headers['X-Limit-D']
elif 'd' in request.query:
data['device'] = request.query['d']
else:
return Response(
body=json.dumps({'error': 'You need to supply device name.'}),
content_type="application/json"
)
message = await request.json()
message['topic'] = '{}/{}/{}'.format(topic, data['user'],
data['device'])
try:
await async_handle_message(hass, context, message)
return Response(body=json.dumps([]), status=200,
content_type="application/json")
except ValueError:
_LOGGER.error("Received invalid JSON")
return None
hass.components.webhook.async_register(
'owntracks', 'OwnTracks', config['webhook_id'], handle_webhook)
return True

View File

@@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.5']
REQUIREMENTS = ['pysnmp==4.4.6']
_LOGGER = logging.getLogger(__name__)

View File

@@ -7,33 +7,32 @@ https://home-assistant.io/components/device_tracker.volvooncall/
import logging
from homeassistant.util import slugify
from homeassistant.helpers.dispatcher import (
dispatcher_connect, dispatcher_send)
from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_VEHICLE_SEEN
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_STATE_UPDATED
_LOGGER = logging.getLogger(__name__)
def setup_scanner(hass, config, see, discovery_info=None):
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the Volvo tracker."""
if discovery_info is None:
return
vin, _ = discovery_info
voc = hass.data[DATA_KEY]
vehicle = voc.vehicles[vin]
vin, component, attr = discovery_info
data = hass.data[DATA_KEY]
instrument = data.instrument(vin, component, attr)
def see_vehicle(vehicle):
async def see_vehicle():
"""Handle the reporting of the vehicle position."""
host_name = voc.vehicle_name(vehicle)
host_name = instrument.vehicle_name
dev_id = 'volvo_{}'.format(slugify(host_name))
see(dev_id=dev_id,
host_name=host_name,
gps=(vehicle.position['latitude'],
vehicle.position['longitude']),
icon='mdi:car')
await async_see(dev_id=dev_id,
host_name=host_name,
source_type=SOURCE_TYPE_GPS,
gps=instrument.state,
icon='mdi:car')
dispatcher_connect(hass, SIGNAL_VEHICLE_SEEN, see_vehicle)
dispatcher_send(hass, SIGNAL_VEHICLE_SEEN, vehicle)
async_dispatcher_connect(hass, SIGNAL_STATE_UPDATED, see_vehicle)
return True

View File

@@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import CONF_HOST, CONF_TOKEN
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-miio==0.4.3', 'construct==2.9.45']
REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__)

View File

@@ -5,7 +5,7 @@
"one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
},
"create_entry": {
"default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/json\n\nConsulteu [la documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
"default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/json\n\nVegeu [la documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
},
"step": {
"user": {

View File

@@ -0,0 +1,10 @@
{
"config": {
"step": {
"user": {
"title": "Dialogflow Webhook be\u00e1ll\u00edt\u00e1sa"
}
},
"title": "Dialogflow"
}
}

View File

@@ -5,7 +5,7 @@
"one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
},
"create_entry": {
"default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow Webhook]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694."
"default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow Webhook]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694."
},
"step": {
"user": {

View File

@@ -10,7 +10,7 @@
"step": {
"user": {
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Dialogflow?",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Dialogflow Webhook"
"title": "Dialogflow Webhook"
}
},
"title": "Dialogflow"

View File

@@ -134,6 +134,7 @@ async def async_setup(hass, config):
discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered:
logger.debug("Already discoverd service %s %s.", service, info)
return
already_discovered.add(discovery_hash)

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType # noqa
DOMAIN = "elkm1"
REQUIREMENTS = ['elkm1-lib==0.7.12']
REQUIREMENTS = ['elkm1-lib==0.7.13']
CONF_AREA = 'area'
CONF_COUNTER = 'counter'

View File

@@ -97,8 +97,8 @@ async def async_setup(hass, yaml_config):
app._on_startup.freeze()
await app.startup()
handler = None
server = None
runner = None
site = None
DescriptionXmlView(config).register(app, app.router)
HueUsernameView().register(app, app.router)
@@ -115,25 +115,24 @@ async def async_setup(hass, yaml_config):
async def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge."""
upnp_listener.stop()
if server:
server.close()
await server.wait_closed()
await app.shutdown()
if handler:
await handler.shutdown(10)
await app.cleanup()
if site:
await site.stop()
if runner:
await runner.cleanup()
async def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge."""
upnp_listener.start()
nonlocal handler
nonlocal server
nonlocal site
nonlocal runner
handler = app.make_handler(loop=hass.loop)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, config.host_ip_addr, config.listen_port)
try:
server = await hass.loop.create_server(
handler, config.host_ip_addr, config.listen_port)
await site.start()
except OSError as error:
_LOGGER.error("Failed to create HTTP server at port %d: %s",
config.listen_port, error)

View File

@@ -1,4 +1,4 @@
"""Support for Honeywell evohome (EMEA/EU-based systems only).
"""Support for (EMEA/EU-based) Honeywell evohome systems.
Support for a temperature control system (TCS, controller) with 0+ heating
zones (e.g. TRVs, relays) and, optionally, a DHW controller.
@@ -8,46 +8,48 @@ https://home-assistant.io/components/evohome/
"""
# Glossary:
# TCS - temperature control system (a.k.a. Controller, Parent), which can
# have up to 13 Children:
# 0-12 Heating zones (a.k.a. Zone), and
# 0-1 DHW controller, (a.k.a. Boiler)
# TCS - temperature control system (a.k.a. Controller, Parent), which can
# have up to 13 Children:
# 0-12 Heating zones (a.k.a. Zone), and
# 0-1 DHW controller, (a.k.a. Boiler)
# The TCS & Zones are implemented as Climate devices, Boiler as a WaterHeater
from datetime import timedelta
import logging
from requests.exceptions import HTTPError
import voluptuous as vol
from homeassistant.const import (
CONF_USERNAME,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
HTTP_BAD_REQUEST
CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD,
EVENT_HOMEASSISTANT_START,
HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
REQUIREMENTS = ['evohomeclient==0.2.7']
# If ever > 0.2.7, re-check the work-around wrapper is still required when
# instantiating the client, below.
REQUIREMENTS = ['evohomeclient==0.2.8']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'evohome'
DATA_EVOHOME = 'data_' + DOMAIN
DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN
CONF_LOCATION_IDX = 'location_idx'
MAX_TEMP = 28
MIN_TEMP = 5
SCAN_INTERVAL_DEFAULT = 180
SCAN_INTERVAL_MAX = 300
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
SCAN_INTERVAL_MINIMUM = timedelta(seconds=180)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int,
vol.Optional(CONF_LOCATION_IDX, default=0):
cv.positive_int,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT):
vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)),
}),
}, extra=vol.ALLOW_EXTRA)
@@ -55,91 +57,107 @@ CONFIG_SCHEMA = vol.Schema({
GWS = 'gateways'
TCS = 'temperatureControlSystems'
# bit masks for dispatcher packets
EVO_PARENT = 0x01
EVO_CHILD = 0x02
def setup(hass, config):
"""Create a Honeywell (EMEA/EU) evohome CH/DHW system.
One controller with 0+ heating zones (e.g. TRVs, relays) and, optionally, a
DHW controller. Does not work for US-based systems.
def setup(hass, hass_config):
"""Create a (EMEA/EU-based) Honeywell evohome system.
Currently, only the Controller and the Zones are implemented here.
"""
evo_data = hass.data[DATA_EVOHOME] = {}
evo_data['timers'] = {}
evo_data['params'] = dict(config[DOMAIN])
evo_data['params'][CONF_SCAN_INTERVAL] = SCAN_INTERVAL_DEFAULT
# use a copy, since scan_interval is rounded up to nearest 60s
evo_data['params'] = dict(hass_config[DOMAIN])
scan_interval = evo_data['params'][CONF_SCAN_INTERVAL]
scan_interval = timedelta(
minutes=(scan_interval.total_seconds() + 59) // 60)
from evohomeclient2 import EvohomeClient
_LOGGER.debug("setup(): API call [4 request(s)]: client.__init__()...")
try:
# There's a bug in evohomeclient2 v0.2.7: the client.__init__() sets
# the root loglevel when EvohomeClient(debug=?), so remember it now...
log_level = logging.getLogger().getEffectiveLevel()
client = EvohomeClient(
evo_data['params'][CONF_USERNAME],
evo_data['params'][CONF_PASSWORD],
debug=False
)
# ...then restore it to what it was before instantiating the client
logging.getLogger().setLevel(log_level)
except HTTPError as err:
if err.response.status_code == HTTP_BAD_REQUEST:
_LOGGER.error(
"Failed to establish a connection with evohome web servers, "
"setup(): Failed to connect with the vendor's web servers. "
"Check your username (%s), and password are correct."
"Unable to continue. Resolve any errors and restart HA.",
evo_data['params'][CONF_USERNAME]
)
return False # unable to continue
raise # we dont handle any other HTTPErrors
elif err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
_LOGGER.error(
"setup(): Failed to connect with the vendor's web servers. "
"The server is not contactable. Unable to continue. "
"Resolve any errors and restart HA."
)
finally: # Redact username, password as no longer needed.
elif err.response.status_code == HTTP_TOO_MANY_REQUESTS:
_LOGGER.error(
"setup(): Failed to connect with the vendor's web servers. "
"You have exceeded the api rate limit. Unable to continue. "
"Wait a while (say 10 minutes) and restart HA."
)
else:
raise # we dont expect/handle any other HTTPErrors
return False # unable to continue
finally: # Redact username, password as no longer needed
evo_data['params'][CONF_USERNAME] = 'REDACTED'
evo_data['params'][CONF_PASSWORD] = 'REDACTED'
evo_data['client'] = client
evo_data['status'] = {}
# Redact any installation data we'll never need.
if client.installation_info[0]['locationInfo']['locationId'] != 'REDACTED':
for loc in client.installation_info:
loc['locationInfo']['streetAddress'] = 'REDACTED'
loc['locationInfo']['city'] = 'REDACTED'
loc['locationInfo']['locationOwner'] = 'REDACTED'
loc[GWS][0]['gatewayInfo'] = 'REDACTED'
# Redact any installation data we'll never need
for loc in client.installation_info:
loc['locationInfo']['locationId'] = 'REDACTED'
loc['locationInfo']['locationOwner'] = 'REDACTED'
loc['locationInfo']['streetAddress'] = 'REDACTED'
loc['locationInfo']['city'] = 'REDACTED'
loc[GWS][0]['gatewayInfo'] = 'REDACTED'
# Pull down the installation configuration.
# Pull down the installation configuration
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
try:
evo_data['config'] = client.installation_info[loc_idx]
except IndexError:
_LOGGER.warning(
"setup(): Parameter '%s' = %s , is outside its range (0-%s)",
"setup(): Parameter '%s'=%s, is outside its range (0-%s)",
CONF_LOCATION_IDX,
loc_idx,
len(client.installation_info) - 1
)
return False # unable to continue
evo_data['status'] = {}
if _LOGGER.isEnabledFor(logging.DEBUG):
tmp_loc = dict(evo_data['config'])
tmp_loc['locationInfo']['postcode'] = 'REDACTED'
tmp_tcs = tmp_loc[GWS][0][TCS][0]
if 'zones' in tmp_tcs:
tmp_tcs['zones'] = '...'
if 'dhw' in tmp_tcs:
tmp_tcs['dhw'] = '...'
if 'dhw' in tmp_loc[GWS][0][TCS][0]: # if this location has DHW...
tmp_loc[GWS][0][TCS][0]['dhw'] = '...'
_LOGGER.debug("setup(), location = %s", tmp_loc)
_LOGGER.debug("setup(): evo_data['config']=%s", tmp_loc)
load_platform(hass, 'climate', DOMAIN, {}, config)
load_platform(hass, 'climate', DOMAIN, {}, hass_config)
@callback
def _first_update(event):
# When HA has started, the hub knows to retreive it's first update
pkt = {'sender': 'setup()', 'signal': 'refresh', 'to': EVO_PARENT}
async_dispatcher_send(hass, DISPATCHER_EVOHOME, pkt)
hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update)
return True

View File

@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.mqtt/
"""
import logging
from typing import Optional
import voluptuous as vol
@@ -18,7 +17,7 @@ from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate,
MqttEntityDeviceInfo)
MqttEntityDeviceInfo, subscription)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
@@ -107,40 +106,7 @@ async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None):
"""Set up the MQTT fan."""
async_add_entities([MqttFan(
config.get(CONF_NAME),
{
key: config.get(key) for key in (
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
CONF_SPEED_STATE_TOPIC,
CONF_SPEED_COMMAND_TOPIC,
CONF_OSCILLATION_STATE_TOPIC,
CONF_OSCILLATION_COMMAND_TOPIC,
)
},
{
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE),
OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE)
},
config.get(CONF_QOS),
config.get(CONF_RETAIN),
{
STATE_ON: config.get(CONF_PAYLOAD_ON),
STATE_OFF: config.get(CONF_PAYLOAD_OFF),
OSCILLATE_ON_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_ON),
OSCILLATE_OFF_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_OFF),
SPEED_LOW: config.get(CONF_PAYLOAD_LOW_SPEED),
SPEED_MEDIUM: config.get(CONF_PAYLOAD_MEDIUM_SPEED),
SPEED_HIGH: config.get(CONF_PAYLOAD_HIGH_SPEED),
},
config.get(CONF_SPEED_LIST),
config.get(CONF_OPTIMISTIC),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_UNIQUE_ID),
config.get(CONF_DEVICE),
config,
discovery_hash,
)])
@@ -149,43 +115,95 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
FanEntity):
"""A MQTT fan component."""
def __init__(self, name, topic, templates, qos, retain, payload,
speed_list, optimistic, availability_topic, payload_available,
payload_not_available, unique_id: Optional[str],
device_config: Optional[ConfigType], discovery_hash):
def __init__(self, config, discovery_hash):
"""Initialize the MQTT fan."""
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
MqttEntityDeviceInfo.__init__(self, device_config)
self._name = name
self._topic = topic
self._qos = qos
self._retain = retain
self._payload = payload
self._templates = templates
self._speed_list = speed_list
self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None
self._optimistic_oscillation = (
optimistic or topic[CONF_OSCILLATION_STATE_TOPIC] is None)
self._optimistic_speed = (
optimistic or topic[CONF_SPEED_STATE_TOPIC] is None)
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = False
self._speed = None
self._oscillation = None
self._supported_features = 0
self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC]
is not None and SUPPORT_OSCILLATE)
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
is not None and SUPPORT_SET_SPEED)
self._unique_id = unique_id
self._discovery_hash = discovery_hash
self._sub_state = None
self._topic = None
self._payload = None
self._templates = None
self._optimistic = None
self._optimistic_oscillation = None
self._optimistic_speed = None
# Load config
self._setup_from_config(config)
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE)
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update)
MqttEntityDeviceInfo.__init__(self, device_config)
async def async_added_to_hass(self):
"""Subscribe to MQTT events."""
await MqttAvailability.async_added_to_hass(self)
await MqttDiscoveryUpdate.async_added_to_hass(self)
await super().async_added_to_hass()
await self._subscribe_topics()
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._setup_from_config(config)
await self.availability_discovery_update(config)
await self._subscribe_topics()
self.async_schedule_update_ha_state()
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
self._config = config
self._topic = {
key: config.get(key) for key in (
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
CONF_SPEED_STATE_TOPIC,
CONF_SPEED_COMMAND_TOPIC,
CONF_OSCILLATION_STATE_TOPIC,
CONF_OSCILLATION_COMMAND_TOPIC,
)
}
self._templates = {
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE),
OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE)
}
self._payload = {
STATE_ON: config.get(CONF_PAYLOAD_ON),
STATE_OFF: config.get(CONF_PAYLOAD_OFF),
OSCILLATE_ON_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_ON),
OSCILLATE_OFF_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_OFF),
SPEED_LOW: config.get(CONF_PAYLOAD_LOW_SPEED),
SPEED_MEDIUM: config.get(CONF_PAYLOAD_MEDIUM_SPEED),
SPEED_HIGH: config.get(CONF_PAYLOAD_HIGH_SPEED),
}
optimistic = config.get(CONF_OPTIMISTIC)
self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None
self._optimistic_oscillation = (
optimistic or self._topic[CONF_OSCILLATION_STATE_TOPIC] is None)
self._optimistic_speed = (
optimistic or self._topic[CONF_SPEED_STATE_TOPIC] is None)
self._supported_features = 0
self._supported_features |= (self._topic[CONF_OSCILLATION_STATE_TOPIC]
is not None and SUPPORT_OSCILLATE)
self._supported_features |= (self._topic[CONF_SPEED_STATE_TOPIC]
is not None and SUPPORT_SET_SPEED)
self._unique_id = config.get(CONF_UNIQUE_ID)
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
topics = {}
templates = {}
for key, tpl in list(self._templates.items()):
if tpl is None:
@@ -205,9 +223,10 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
self.async_schedule_update_ha_state()
if self._topic[CONF_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_STATE_TOPIC], state_received,
self._qos)
topics[CONF_STATE_TOPIC] = {
'topic': self._topic[CONF_STATE_TOPIC],
'msg_callback': state_received,
'qos': self._config.get(CONF_QOS)}
@callback
def speed_received(topic, payload, qos):
@@ -222,9 +241,10 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
self.async_schedule_update_ha_state()
if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received,
self._qos)
topics[CONF_SPEED_STATE_TOPIC] = {
'topic': self._topic[CONF_SPEED_STATE_TOPIC],
'msg_callback': speed_received,
'qos': self._config.get(CONF_QOS)}
self._speed = SPEED_OFF
@callback
@@ -238,11 +258,21 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
self.async_schedule_update_ha_state()
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC],
oscillation_received, self._qos)
topics[CONF_OSCILLATION_STATE_TOPIC] = {
'topic': self._topic[CONF_OSCILLATION_STATE_TOPIC],
'msg_callback': oscillation_received,
'qos': self._config.get(CONF_QOS)}
self._oscillation = False
self._sub_state = await subscription.async_subscribe_topics(
self.hass, self._sub_state,
topics)
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
await MqttAvailability.async_will_remove_from_hass(self)
@property
def should_poll(self):
"""No polling needed for a MQTT fan."""
@@ -261,12 +291,12 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
@property
def name(self) -> str:
"""Get entity name."""
return self._name
return self._config.get(CONF_NAME)
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return self._speed_list
return self._config.get(CONF_SPEED_LIST)
@property
def supported_features(self) -> int:
@@ -290,7 +320,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
"""
mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_ON], self._qos, self._retain)
self._payload[STATE_ON], self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if speed:
await self.async_set_speed(speed)
@@ -301,7 +332,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
"""
mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_OFF], self._qos, self._retain)
self._payload[STATE_OFF], self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan.
@@ -322,7 +354,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
mqtt.async_publish(
self.hass, self._topic[CONF_SPEED_COMMAND_TOPIC],
mqtt_payload, self._qos, self._retain)
mqtt_payload, self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._optimistic_speed:
self._speed = speed
@@ -343,7 +376,7 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
mqtt.async_publish(
self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
payload, self._qos, self._retain)
payload, self._config.get(CONF_QOS), self._config.get(CONF_RETAIN))
if self._optimistic_oscillation:
self._oscillation = oscillating

View File

@@ -18,7 +18,7 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-miio==0.4.3', 'construct==2.9.45']
REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45']
_LOGGER = logging.getLogger(__name__)
@@ -755,12 +755,13 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
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]
self._speed_list = [mode.name for mode in OperationMode if
mode is not OperationMode.Strong]
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']
mode is not OperationMode.Auto]
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes})

View File

@@ -5,10 +5,15 @@ For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/fan.zha/
"""
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)
DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED,
FanEntity)
from homeassistant.components.zha import helpers
from homeassistant.components.zha.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['zha']
@@ -39,15 +44,38 @@ SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
async def async_setup_platform(hass, config, async_add_entities,
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_entities([ZhaFan(**discovery_info)], update_before_add=True)
"""Old way of setting up Zigbee Home Automation fans."""
pass
class ZhaFan(zha.Entity, FanEntity):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Zigbee Home Automation fan from config entry."""
async def async_discover(discovery_info):
await _async_setup_entities(hass, config_entry, async_add_entities,
[discovery_info])
unsub = async_dispatcher_connect(
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
fans = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
if fans is not None:
await _async_setup_entities(hass, config_entry, async_add_entities,
fans.values())
del hass.data[DATA_ZHA][DOMAIN]
async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos):
"""Set up the ZHA fans."""
entities = []
for discovery_info in discovery_infos:
entities.append(ZhaFan(**discovery_info))
async_add_entities(entities, update_before_add=True)
class ZhaFan(ZhaEntity, FanEntity):
"""Representation of a ZHA fan."""
_domain = DOMAIN
@@ -101,9 +129,9 @@ class ZhaFan(zha.Entity, FanEntity):
async def async_update(self):
"""Retrieve latest state."""
result = await zha.safe_read(self._endpoint.fan, ['fan_mode'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.fan, ['fan_mode'],
allow_cache=False,
only_cache=(not self._initialized))
new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, None)

View File

@@ -7,6 +7,7 @@ https://home-assistant.io/components/fibaro/
import logging
from collections import defaultdict
from typing import Optional
import voluptuous as vol
from homeassistant.const import (ATTR_ARMED, ATTR_BATTERY_LEVEL,
@@ -27,7 +28,8 @@ ATTR_CURRENT_POWER_W = "current_power_w"
ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh"
CONF_PLUGINS = "plugins"
FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light', 'sensor', 'switch']
FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light',
'scene', 'sensor', 'switch']
FIBARO_TYPEMAP = {
'com.fibaro.multilevelSensor': "sensor",
@@ -43,7 +45,8 @@ FIBARO_TYPEMAP = {
'com.fibaro.smokeSensor': 'binary_sensor',
'com.fibaro.remoteSwitch': 'switch',
'com.fibaro.sensor': 'sensor',
'com.fibaro.colorController': 'light'
'com.fibaro.colorController': 'light',
'com.fibaro.securitySensor': 'binary_sensor'
}
CONFIG_SCHEMA = vol.Schema({
@@ -63,19 +66,23 @@ class FibaroController():
_device_map = None # Dict for mapping deviceId to device object
fibaro_devices = None # List of devices by type
_callbacks = {} # Dict of update value callbacks by deviceId
_client = None # Fiblary's Client object for communication
_state_handler = None # Fiblary's StateHandler object
_client = None # Fiblary's Client object for communication
_state_handler = None # Fiblary's StateHandler object
_import_plugins = None # Whether to import devices from plugins
def __init__(self, username, password, url, import_plugins):
"""Initialize the Fibaro controller."""
from fiblary3.client.v4.client import Client as FibaroClient
self._client = FibaroClient(url, username, password)
self._scene_map = None
self.hub_serial = None # Unique serial number of the hub
def connect(self):
"""Start the communication with the Fibaro controller."""
try:
login = self._client.login.get()
info = self._client.info.get()
self.hub_serial = slugify(info.serialNumber)
except AssertionError:
_LOGGER.error("Can't connect to Fibaro HC. "
"Please check URL.")
@@ -87,6 +94,7 @@ class FibaroController():
self._room_map = {room.id: room for room in self._client.rooms.list()}
self._read_devices()
self._read_scenes()
return True
def enable_state_handler(self):
@@ -103,29 +111,31 @@ class FibaroController():
"""Handle change report received from the HomeCenter."""
callback_set = set()
for change in state.get('changes', []):
dev_id = change.pop('id')
for property_name, value in change.items():
if property_name == 'log':
if value and value != "transfer OK":
_LOGGER.debug("LOG %s: %s",
self._device_map[dev_id].friendly_name,
value)
try:
dev_id = change.pop('id')
if dev_id not in self._device_map.keys():
continue
if property_name == 'logTemp':
continue
if property_name in self._device_map[dev_id].properties:
self._device_map[dev_id].properties[property_name] = \
value
_LOGGER.debug("<- %s.%s = %s",
self._device_map[dev_id].ha_id,
property_name,
str(value))
else:
_LOGGER.warning("Error updating %s data of %s, not found",
property_name,
self._device_map[dev_id].ha_id)
if dev_id in self._callbacks:
callback_set.add(dev_id)
device = self._device_map[dev_id]
for property_name, value in change.items():
if property_name == 'log':
if value and value != "transfer OK":
_LOGGER.debug("LOG %s: %s",
device.friendly_name, value)
continue
if property_name == 'logTemp':
continue
if property_name in device.properties:
device.properties[property_name] = \
value
_LOGGER.debug("<- %s.%s = %s", device.ha_id,
property_name, str(value))
else:
_LOGGER.warning("%s.%s not found", device.ha_id,
property_name)
if dev_id in self._callbacks:
callback_set.add(dev_id)
except (ValueError, KeyError):
pass
for item in callback_set:
self._callbacks[item]()
@@ -137,8 +147,12 @@ class FibaroController():
def _map_device_to_type(device):
"""Map device to HA device type."""
# Use our lookup table to identify device type
device_type = FIBARO_TYPEMAP.get(
device.type, FIBARO_TYPEMAP.get(device.baseType))
if 'type' in device:
device_type = FIBARO_TYPEMAP.get(device.type)
elif 'baseType' in device:
device_type = FIBARO_TYPEMAP.get(device.baseType)
else:
device_type = None
# We can also identify device type by its capabilities
if device_type is None:
@@ -156,35 +170,61 @@ class FibaroController():
# Switches that control lights should show up as lights
if device_type == 'switch' and \
'isLight' in device.properties and \
device.properties.isLight == 'true':
device.properties.get('isLight', 'false') == 'true':
device_type = 'light'
return device_type
def _read_scenes(self):
scenes = self._client.scenes.list()
self._scene_map = {}
for device in scenes:
if not device.visible:
continue
if device.roomID == 0:
room_name = 'Unknown'
else:
room_name = self._room_map[device.roomID].name
device.room_name = room_name
device.friendly_name = '{} {}'.format(room_name, device.name)
device.ha_id = '{}_{}_{}'.format(
slugify(room_name), slugify(device.name), device.id)
device.unique_id_str = "{}.{}".format(
self.hub_serial, device.id)
self._scene_map[device.id] = device
self.fibaro_devices['scene'].append(device)
def _read_devices(self):
"""Read and process the device list."""
devices = self._client.devices.list()
self._device_map = {}
for device in devices:
if device.roomID == 0:
room_name = 'Unknown'
else:
room_name = self._room_map[device.roomID].name
device.friendly_name = room_name + ' ' + device.name
device.ha_id = '{}_{}_{}'.format(
slugify(room_name), slugify(device.name), device.id)
self._device_map[device.id] = device
self.fibaro_devices = defaultdict(list)
for device in self._device_map.values():
if device.enabled and \
(not device.isPlugin or self._import_plugins):
device.mapped_type = self._map_device_to_type(device)
for device in devices:
try:
if device.roomID == 0:
room_name = 'Unknown'
else:
room_name = self._room_map[device.roomID].name
device.room_name = room_name
device.friendly_name = room_name + ' ' + device.name
device.ha_id = '{}_{}_{}'.format(
slugify(room_name), slugify(device.name), device.id)
if device.enabled and \
('isPlugin' not in device or
(not device.isPlugin or self._import_plugins)):
device.mapped_type = self._map_device_to_type(device)
else:
device.mapped_type = None
if device.mapped_type:
device.unique_id_str = "{}.{}".format(
self.hub_serial, device.id)
self._device_map[device.id] = device
self.fibaro_devices[device.mapped_type].append(device)
else:
_LOGGER.debug("%s (%s, %s) not mapped",
_LOGGER.debug("%s (%s, %s) not used",
device.ha_id, device.type,
device.baseType)
except (KeyError, ValueError):
pass
def setup(hass, config):
@@ -273,11 +313,14 @@ class FibaroDevice(Entity):
def call_set_color(self, red, green, blue, white):
"""Set the color of Fibaro device."""
color_str = "{},{},{},{}".format(int(red), int(green),
int(blue), int(white))
red = int(max(0, min(255, red)))
green = int(max(0, min(255, green)))
blue = int(max(0, min(255, blue)))
white = int(max(0, min(255, white)))
color_str = "{},{},{},{}".format(red, green, blue, white)
self.fibaro_device.properties.color = color_str
self.action("setColor", str(int(red)), str(int(green)),
str(int(blue)), str(int(white)))
self.action("setColor", str(red), str(green),
str(blue), str(white))
def action(self, cmd, *args):
"""Perform an action on the Fibaro HC."""
@@ -314,7 +357,12 @@ class FibaroDevice(Entity):
return False
@property
def name(self):
def unique_id(self) -> str:
"""Return a unique ID."""
return self.fibaro_device.unique_id_str
@property
def name(self) -> Optional[str]:
"""Return the name of the device."""
return self._name
@@ -347,5 +395,5 @@ class FibaroDevice(Entity):
except (ValueError, KeyError):
pass
attr['id'] = self.ha_id
attr['fibaro_id'] = self.fibaro_device.id
return attr

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==20181121.0']
REQUIREMENTS = ['home-assistant-frontend==20181211.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
@@ -238,7 +238,7 @@ async def async_setup(hass, config):
if os.path.isdir(local):
hass.http.register_static_path("/local", local, not is_dev)
index_view = IndexView(repo_path, js_version, hass.auth.active)
index_view = IndexView(repo_path, js_version)
hass.http.register_view(index_view)
hass.http.register_view(AuthorizeView(repo_path, js_version))
@@ -250,7 +250,7 @@ async def async_setup(hass, config):
await asyncio.wait(
[async_register_built_in_panel(hass, panel) for panel in (
'dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template', 'dev-mqtt', 'kiosk', 'lovelace', 'profile')],
'dev-template', 'dev-mqtt', 'kiosk', 'states', 'profile')],
loop=hass.loop)
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
@@ -362,13 +362,11 @@ class IndexView(HomeAssistantView):
url = '/'
name = 'frontend:index'
requires_auth = False
extra_urls = ['/states', '/states/{extra}']
def __init__(self, repo_path, js_option, auth_active):
def __init__(self, repo_path, js_option):
"""Initialize the frontend view."""
self.repo_path = repo_path
self.js_option = js_option
self.auth_active = auth_active
self._template_cache = {}
def get_template(self, latest):
@@ -415,8 +413,6 @@ class IndexView(HomeAssistantView):
# do not try to auto connect on load
no_auth = '0'
use_oauth = '1' if self.auth_active else '0'
template = await hass.async_add_job(self.get_template, latest)
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
@@ -425,7 +421,7 @@ class IndexView(HomeAssistantView):
no_auth=no_auth,
theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[extra_key],
use_oauth=use_oauth
use_oauth='1'
)
return web.Response(text=template.render(**template_params),

View File

@@ -13,7 +13,8 @@ import voluptuous as vol
from homeassistant.components.geo_location import (
PLATFORM_SCHEMA, GeoLocationEvent)
from homeassistant.const import (
CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START)
CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START,
CONF_LATITUDE, CONF_LONGITUDE)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
@@ -38,6 +39,8 @@ SOURCE = 'geo_json_events'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
})
@@ -46,10 +49,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the GeoJSON Events platform."""
url = config[CONF_URL]
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
coordinates = (config.get(CONF_LATITUDE, hass.config.latitude),
config.get(CONF_LONGITUDE, hass.config.longitude))
radius_in_km = config[CONF_RADIUS]
# Initialize the entity manager.
feed = GeoJsonFeedManager(hass, add_entities, scan_interval, url,
radius_in_km)
feed = GeoJsonFeedEntityManager(
hass, add_entities, scan_interval, coordinates, url, radius_in_km)
def start_feed_manager(event):
"""Start feed manager."""
@@ -58,87 +63,49 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
class GeoJsonFeedManager:
"""Feed Manager for GeoJSON feeds."""
class GeoJsonFeedEntityManager:
"""Feed Entity Manager for GeoJSON feeds."""
def __init__(self, hass, add_entities, scan_interval, url, radius_in_km):
def __init__(self, hass, add_entities, scan_interval, coordinates, url,
radius_in_km):
"""Initialize the GeoJSON Feed Manager."""
from geojson_client.generic_feed import GenericFeed
from geojson_client.generic_feed import GenericFeedManager
self._hass = hass
self._feed = GenericFeed(
(hass.config.latitude, hass.config.longitude),
filter_radius=radius_in_km, url=url)
self._feed_manager = GenericFeedManager(
self._generate_entity, self._update_entity, self._remove_entity,
coordinates, url, filter_radius=radius_in_km)
self._add_entities = add_entities
self._scan_interval = scan_interval
self.feed_entries = {}
self._managed_external_ids = set()
def startup(self):
"""Start up this manager."""
self._update()
self._feed_manager.update()
self._init_regular_updates()
def _init_regular_updates(self):
"""Schedule regular updates at the specified interval."""
track_time_interval(
self._hass, lambda now: self._update(), self._scan_interval)
self._hass, lambda now: self._feed_manager.update(),
self._scan_interval)
def _update(self):
"""Update the feed and then update connected entities."""
import geojson_client
def get_entry(self, external_id):
"""Get feed entry by external id."""
return self._feed_manager.feed_entries.get(external_id)
status, feed_entries = self._feed.update()
if status == geojson_client.UPDATE_OK:
_LOGGER.debug("Data retrieved %s", feed_entries)
# Keep a copy of all feed entries for future lookups by entities.
self.feed_entries = {entry.external_id: entry
for entry in feed_entries}
# For entity management the external ids from the feed are used.
feed_external_ids = set(self.feed_entries)
remove_external_ids = self._managed_external_ids.difference(
feed_external_ids)
self._remove_entities(remove_external_ids)
update_external_ids = self._managed_external_ids.intersection(
feed_external_ids)
self._update_entities(update_external_ids)
create_external_ids = feed_external_ids.difference(
self._managed_external_ids)
self._generate_new_entities(create_external_ids)
elif status == geojson_client.UPDATE_OK_NO_DATA:
_LOGGER.debug(
"Update successful, but no data received from %s", self._feed)
else:
_LOGGER.warning(
"Update not successful, no data received from %s", self._feed)
# Remove all entities.
self._remove_entities(self._managed_external_ids.copy())
def _generate_new_entities(self, external_ids):
"""Generate new entities for events."""
new_entities = []
for external_id in external_ids:
new_entity = GeoJsonLocationEvent(self, external_id)
_LOGGER.debug("New entity added %s", external_id)
new_entities.append(new_entity)
self._managed_external_ids.add(external_id)
def _generate_entity(self, external_id):
"""Generate new entity."""
new_entity = GeoJsonLocationEvent(self, external_id)
# Add new entities to HA.
self._add_entities(new_entities, True)
self._add_entities([new_entity], True)
def _update_entities(self, external_ids):
"""Update entities."""
for external_id in external_ids:
_LOGGER.debug("Existing entity found %s", external_id)
dispatcher_send(
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _update_entity(self, external_id):
"""Update entity."""
dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _remove_entities(self, external_ids):
"""Remove entities."""
for external_id in external_ids:
_LOGGER.debug("Entity not current anymore %s", external_id)
self._managed_external_ids.remove(external_id)
dispatcher_send(
self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
def _remove_entity(self, external_id):
"""Remove entity."""
dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
class GeoJsonLocationEvent(GeoLocationEvent):
@@ -184,7 +151,7 @@ class GeoJsonLocationEvent(GeoLocationEvent):
async def async_update(self):
"""Update this entity from the data held in the feed manager."""
_LOGGER.debug("Updating %s", self._external_id)
feed_entry = self._feed_manager.feed_entries.get(self._external_id)
feed_entry = self._feed_manager.get_entry(self._external_id)
if feed_entry:
self._update_from_feed(feed_entry)

View File

@@ -14,7 +14,7 @@ from homeassistant.components.geo_location import (
PLATFORM_SCHEMA, GeoLocationEvent)
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_START)
EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
@@ -57,18 +57,23 @@ VALID_CATEGORIES = [
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_CATEGORIES, default=[]):
vol.All(cv.ensure_list, [vol.In(VALID_CATEGORIES)]),
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the GeoJSON Events platform."""
"""Set up the NSW Rural Fire Service Feed platform."""
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
coordinates = (config.get(CONF_LATITUDE, hass.config.latitude),
config.get(CONF_LONGITUDE, hass.config.longitude))
radius_in_km = config[CONF_RADIUS]
categories = config.get(CONF_CATEGORIES)
# Initialize the entity manager.
feed = NswRuralFireServiceFeedManager(
hass, add_entities, scan_interval, radius_in_km, categories)
feed = NswRuralFireServiceFeedEntityManager(
hass, add_entities, scan_interval, coordinates, radius_in_km,
categories)
def start_feed_manager(event):
"""Start feed manager."""
@@ -77,93 +82,55 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
class NswRuralFireServiceFeedManager:
"""Feed Manager for NSW Rural Fire Service GeoJSON feed."""
class NswRuralFireServiceFeedEntityManager:
"""Feed Entity Manager for NSW Rural Fire Service GeoJSON feed."""
def __init__(self, hass, add_entities, scan_interval, radius_in_km,
categories):
"""Initialize the GeoJSON Feed Manager."""
def __init__(self, hass, add_entities, scan_interval, coordinates,
radius_in_km, categories):
"""Initialize the Feed Entity Manager."""
from geojson_client.nsw_rural_fire_service_feed \
import NswRuralFireServiceFeed
import NswRuralFireServiceFeedManager
self._hass = hass
self._feed = NswRuralFireServiceFeed(
(hass.config.latitude, hass.config.longitude),
filter_radius=radius_in_km, filter_categories=categories)
self._feed_manager = NswRuralFireServiceFeedManager(
self._generate_entity, self._update_entity, self._remove_entity,
coordinates, filter_radius=radius_in_km,
filter_categories=categories)
self._add_entities = add_entities
self._scan_interval = scan_interval
self.feed_entries = {}
self._managed_external_ids = set()
def startup(self):
"""Start up this manager."""
self._update()
self._feed_manager.update()
self._init_regular_updates()
def _init_regular_updates(self):
"""Schedule regular updates at the specified interval."""
track_time_interval(
self._hass, lambda now: self._update(), self._scan_interval)
self._hass, lambda now: self._feed_manager.update(),
self._scan_interval)
def _update(self):
"""Update the feed and then update connected entities."""
import geojson_client
def get_entry(self, external_id):
"""Get feed entry by external id."""
return self._feed_manager.feed_entries.get(external_id)
status, feed_entries = self._feed.update()
if status == geojson_client.UPDATE_OK:
_LOGGER.debug("Data retrieved %s", feed_entries)
# Keep a copy of all feed entries for future lookups by entities.
self.feed_entries = {entry.external_id: entry
for entry in feed_entries}
# For entity management the external ids from the feed are used.
feed_external_ids = set(self.feed_entries)
remove_external_ids = self._managed_external_ids.difference(
feed_external_ids)
self._remove_entities(remove_external_ids)
update_external_ids = self._managed_external_ids.intersection(
feed_external_ids)
self._update_entities(update_external_ids)
create_external_ids = feed_external_ids.difference(
self._managed_external_ids)
self._generate_new_entities(create_external_ids)
elif status == geojson_client.UPDATE_OK_NO_DATA:
_LOGGER.debug(
"Update successful, but no data received from %s", self._feed)
else:
_LOGGER.warning(
"Update not successful, no data received from %s", self._feed)
# Remove all entities.
self._remove_entities(self._managed_external_ids.copy())
def _generate_new_entities(self, external_ids):
"""Generate new entities for events."""
new_entities = []
for external_id in external_ids:
new_entity = NswRuralFireServiceLocationEvent(self, external_id)
_LOGGER.debug("New entity added %s", external_id)
new_entities.append(new_entity)
self._managed_external_ids.add(external_id)
def _generate_entity(self, external_id):
"""Generate new entity."""
new_entity = NswRuralFireServiceLocationEvent(self, external_id)
# Add new entities to HA.
self._add_entities(new_entities, True)
self._add_entities([new_entity], True)
def _update_entities(self, external_ids):
"""Update entities."""
for external_id in external_ids:
_LOGGER.debug("Existing entity found %s", external_id)
dispatcher_send(
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _update_entity(self, external_id):
"""Update entity."""
dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _remove_entities(self, external_ids):
"""Remove entities."""
for external_id in external_ids:
_LOGGER.debug("Entity not current anymore %s", external_id)
self._managed_external_ids.remove(external_id)
dispatcher_send(
self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
def _remove_entity(self, external_id):
"""Remove entity."""
dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
class NswRuralFireServiceLocationEvent(GeoLocationEvent):
"""This represents an external event with GeoJSON data."""
"""This represents an external event with NSW Rural Fire Service data."""
def __init__(self, feed_manager, external_id):
"""Initialize entity with data from feed entry."""
@@ -209,13 +176,13 @@ class NswRuralFireServiceLocationEvent(GeoLocationEvent):
@property
def should_poll(self):
"""No polling needed for GeoJSON location events."""
"""No polling needed for NSW Rural Fire Service location events."""
return False
async def async_update(self):
"""Update this entity from the data held in the feed manager."""
_LOGGER.debug("Updating %s", self._external_id)
feed_entry = self._feed_manager.feed_entries.get(self._external_id)
feed_entry = self._feed_manager.get_entry(self._external_id)
if feed_entry:
self._update_from_feed(feed_entry)

View File

@@ -0,0 +1,268 @@
"""
U.S. Geological Survey Earthquake Hazards Program Feed platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/geo_location/usgs_earthquakes_feed/
"""
from datetime import timedelta
import logging
from typing import Optional
import voluptuous as vol
from homeassistant.components.geo_location import (
PLATFORM_SCHEMA, GeoLocationEvent)
from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_RADIUS, CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, dispatcher_send)
from homeassistant.helpers.event import track_time_interval
REQUIREMENTS = ['geojson_client==0.3']
_LOGGER = logging.getLogger(__name__)
ATTR_ALERT = 'alert'
ATTR_EXTERNAL_ID = 'external_id'
ATTR_MAGNITUDE = 'magnitude'
ATTR_PLACE = 'place'
ATTR_STATUS = 'status'
ATTR_TIME = 'time'
ATTR_TYPE = 'type'
ATTR_UPDATED = 'updated'
CONF_FEED_TYPE = 'feed_type'
CONF_MINIMUM_MAGNITUDE = 'minimum_magnitude'
DEFAULT_MINIMUM_MAGNITUDE = 0.0
DEFAULT_RADIUS_IN_KM = 50.0
DEFAULT_UNIT_OF_MEASUREMENT = 'km'
SCAN_INTERVAL = timedelta(minutes=5)
SIGNAL_DELETE_ENTITY = 'usgs_earthquakes_feed_delete_{}'
SIGNAL_UPDATE_ENTITY = 'usgs_earthquakes_feed_update_{}'
SOURCE = 'usgs_earthquakes_feed'
VALID_FEED_TYPES = [
'past_hour_significant_earthquakes',
'past_hour_m45_earthquakes',
'past_hour_m25_earthquakes',
'past_hour_m10_earthquakes',
'past_hour_all_earthquakes',
'past_day_significant_earthquakes',
'past_day_m45_earthquakes',
'past_day_m25_earthquakes',
'past_day_m10_earthquakes',
'past_day_all_earthquakes',
'past_week_significant_earthquakes',
'past_week_m45_earthquakes',
'past_week_m25_earthquakes',
'past_week_m10_earthquakes',
'past_week_all_earthquakes',
'past_month_significant_earthquakes',
'past_month_m45_earthquakes',
'past_month_m25_earthquakes',
'past_month_m10_earthquakes',
'past_month_all_earthquakes',
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FEED_TYPE): vol.In(VALID_FEED_TYPES),
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
vol.Optional(CONF_MINIMUM_MAGNITUDE, default=DEFAULT_MINIMUM_MAGNITUDE):
vol.All(vol.Coerce(float), vol.Range(min=0))
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the USGS Earthquake Hazards Program Feed platform."""
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
feed_type = config[CONF_FEED_TYPE]
coordinates = (config.get(CONF_LATITUDE, hass.config.latitude),
config.get(CONF_LONGITUDE, hass.config.longitude))
radius_in_km = config[CONF_RADIUS]
minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE]
# Initialize the entity manager.
feed = UsgsEarthquakesFeedEntityManager(
hass, add_entities, scan_interval, coordinates, feed_type,
radius_in_km, minimum_magnitude)
def start_feed_manager(event):
"""Start feed manager."""
feed.startup()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
class UsgsEarthquakesFeedEntityManager:
"""Feed Entity Manager for USGS Earthquake Hazards Program feed."""
def __init__(self, hass, add_entities, scan_interval, coordinates,
feed_type, radius_in_km, minimum_magnitude):
"""Initialize the Feed Entity Manager."""
from geojson_client.usgs_earthquake_hazards_program_feed \
import UsgsEarthquakeHazardsProgramFeedManager
self._hass = hass
self._feed_manager = UsgsEarthquakeHazardsProgramFeedManager(
self._generate_entity, self._update_entity, self._remove_entity,
coordinates, feed_type, filter_radius=radius_in_km,
filter_minimum_magnitude=minimum_magnitude)
self._add_entities = add_entities
self._scan_interval = scan_interval
def startup(self):
"""Start up this manager."""
self._feed_manager.update()
self._init_regular_updates()
def _init_regular_updates(self):
"""Schedule regular updates at the specified interval."""
track_time_interval(
self._hass, lambda now: self._feed_manager.update(),
self._scan_interval)
def get_entry(self, external_id):
"""Get feed entry by external id."""
return self._feed_manager.feed_entries.get(external_id)
def _generate_entity(self, external_id):
"""Generate new entity."""
new_entity = UsgsEarthquakesEvent(self, external_id)
# Add new entities to HA.
self._add_entities([new_entity], True)
def _update_entity(self, external_id):
"""Update entity."""
dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _remove_entity(self, external_id):
"""Remove entity."""
dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
class UsgsEarthquakesEvent(GeoLocationEvent):
"""This represents an external event with USGS Earthquake data."""
def __init__(self, feed_manager, external_id):
"""Initialize entity with data from feed entry."""
self._feed_manager = feed_manager
self._external_id = external_id
self._name = None
self._distance = None
self._latitude = None
self._longitude = None
self._attribution = None
self._place = None
self._magnitude = None
self._time = None
self._updated = None
self._status = None
self._type = None
self._alert = None
self._remove_signal_delete = None
self._remove_signal_update = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self._remove_signal_delete = async_dispatcher_connect(
self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id),
self._delete_callback)
self._remove_signal_update = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id),
self._update_callback)
@callback
def _delete_callback(self):
"""Remove this entity."""
self._remove_signal_delete()
self._remove_signal_update()
self.hass.async_create_task(self.async_remove())
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def should_poll(self):
"""No polling needed for USGS Earthquake events."""
return False
async def async_update(self):
"""Update this entity from the data held in the feed manager."""
_LOGGER.debug("Updating %s", self._external_id)
feed_entry = self._feed_manager.get_entry(self._external_id)
if feed_entry:
self._update_from_feed(feed_entry)
def _update_from_feed(self, feed_entry):
"""Update the internal state from the provided feed entry."""
self._name = feed_entry.title
self._distance = feed_entry.distance_to_home
self._latitude = feed_entry.coordinates[0]
self._longitude = feed_entry.coordinates[1]
self._attribution = feed_entry.attribution
self._place = feed_entry.place
self._magnitude = feed_entry.magnitude
self._time = feed_entry.time
self._updated = feed_entry.updated
self._status = feed_entry.status
self._type = feed_entry.type
self._alert = feed_entry.alert
@property
def source(self) -> str:
"""Return source value of this external event."""
return SOURCE
@property
def name(self) -> Optional[str]:
"""Return the name of the entity."""
return self._name
@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return self._distance
@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return self._latitude
@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return self._longitude
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return DEFAULT_UNIT_OF_MEASUREMENT
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for key, value in (
(ATTR_EXTERNAL_ID, self._external_id),
(ATTR_PLACE, self._place),
(ATTR_MAGNITUDE, self._magnitude),
(ATTR_TIME, self._time),
(ATTR_UPDATED, self._updated),
(ATTR_STATUS, self._status),
(ATTR_TYPE, self._type),
(ATTR_ALERT, self._alert),
(ATTR_ATTRIBUTION, self._attribution),
):
if value or isinstance(value, bool):
attributes[key] = value
return attributes

View File

@@ -12,8 +12,9 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
REQUIREMENTS = ['pysher==1.0.4']
# Version downgraded due to regression in library
# For details: https://github.com/nlsdfnbch/Pysher/issues/38
REQUIREMENTS = ['pysher==1.0.1']
DOMAIN = 'goalfeed'
CONFIG_SCHEMA = vol.Schema({

View File

@@ -33,8 +33,6 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
DEFAULT_AGENT_USER_ID = 'home-assistant'
ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_EXPOSE): cv.boolean,
@@ -70,10 +68,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
websession = async_get_clientsession(hass)
try:
with async_timeout.timeout(5, loop=hass.loop):
agent_user_id = call.data.get('agent_user_id') or \
call.context.user_id
res = await websession.post(
REQUEST_SYNC_BASE_URL,
params={'key': api_key},
json={'agent_user_id': call.context.user_id})
json={'agent_user_id': agent_user_id})
_LOGGER.info("Submitted request_sync request to Google")
res.raise_for_status()
except aiohttp.ClientResponseError:

View File

@@ -19,8 +19,6 @@ DEFAULT_EXPOSED_DOMAINS = [
'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock',
]
DEFAULT_ALLOW_UNLOCK = False
CLIMATE_MODE_HEATCOOL = 'heatcool'
CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL}
PREFIX_TYPES = 'action.devices.types.'
TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'

View File

@@ -16,8 +16,8 @@ class SmartHomeError(Exception):
class Config:
"""Hold the configuration for Google Assistant."""
def __init__(self, should_expose, agent_user_id, entity_config=None,
allow_unlock=False):
def __init__(self, should_expose, allow_unlock, agent_user_id,
entity_config=None):
"""Initialize the configuration."""
self.should_expose = should_expose
self.agent_user_id = agent_user_id

View File

@@ -15,6 +15,7 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from .const import (
GOOGLE_ASSISTANT_API_ENDPOINT,
CONF_ALLOW_UNLOCK,
CONF_EXPOSE_BY_DEFAULT,
CONF_EXPOSED_DOMAINS,
CONF_ENTITY_CONFIG,
@@ -32,6 +33,7 @@ def async_register_http(hass, cfg):
expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT)
exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS)
entity_config = cfg.get(CONF_ENTITY_CONFIG) or {}
allow_unlock = cfg.get(CONF_ALLOW_UNLOCK, False)
def is_exposed(entity) -> bool:
"""Determine if an entity should be exposed to Google Assistant."""
@@ -46,7 +48,7 @@ def async_register_http(hass, cfg):
entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE)
domain_exposed_by_default = \
expose_by_default or entity.domain in exposed_domains
expose_by_default and entity.domain in exposed_domains
# Expose an entity if the entity's domain is exposed by default and
# the configuration doesn't explicitly exclude it from being
@@ -57,7 +59,7 @@ def async_register_http(hass, cfg):
return is_default_exposed or explicit_expose
hass.http.register_view(
GoogleAssistantView(is_exposed, entity_config))
GoogleAssistantView(is_exposed, entity_config, allow_unlock))
class GoogleAssistantView(HomeAssistantView):
@@ -67,15 +69,17 @@ class GoogleAssistantView(HomeAssistantView):
name = 'api:google_assistant'
requires_auth = True
def __init__(self, is_exposed, entity_config):
def __init__(self, is_exposed, entity_config, allow_unlock):
"""Initialize the Google Assistant request handler."""
self.is_exposed = is_exposed
self.entity_config = entity_config
self.allow_unlock = allow_unlock
async def post(self, request: Request) -> Response:
"""Handle Google Assistant requests."""
message = await request.json() # type: dict
config = Config(self.is_exposed,
self.allow_unlock,
request['hass_user'].id,
self.entity_config)
result = await async_handle_message(

View File

@@ -1,2 +1,5 @@
request_sync:
description: Send a request_sync command to Google.
description: Send a request_sync command to Google.
fields:
agent_user_id:
description: Optional. Only needed for automations. Specific Home Assistant user id to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing.

View File

@@ -43,6 +43,7 @@ TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock'
TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
PREFIX_COMMANDS = 'action.devices.commands.'
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
@@ -59,7 +60,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
TRAITS = []
@@ -197,6 +198,8 @@ class OnOffTrait(_Trait):
@staticmethod
def supported(domain, features):
"""Test if state is supported."""
if domain == climate.DOMAIN:
return features & climate.SUPPORT_ON_OFF != 0
return domain in (
group.DOMAIN,
input_boolean.DOMAIN,
@@ -515,6 +518,9 @@ class TemperatureSettingTrait(_Trait):
climate.STATE_COOL: 'cool',
climate.STATE_OFF: 'off',
climate.STATE_AUTO: 'heatcool',
climate.STATE_FAN_ONLY: 'fan-only',
climate.STATE_DRY: 'dry',
climate.STATE_ECO: 'eco'
}
google_to_hass = {value: key for key, value in hass_to_google.items()}
@@ -585,8 +591,11 @@ class TemperatureSettingTrait(_Trait):
max_temp = self.state.attributes[climate.ATTR_MAX_TEMP]
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
temp = temp_util.convert(params['thermostatTemperatureSetpoint'],
TEMP_CELSIUS, unit)
temp = temp_util.convert(
params['thermostatTemperatureSetpoint'], TEMP_CELSIUS,
unit)
if unit == TEMP_FAHRENHEIT:
temp = round(temp)
if temp < min_temp or temp > max_temp:
raise SmartHomeError(
@@ -604,6 +613,8 @@ class TemperatureSettingTrait(_Trait):
temp_high = temp_util.convert(
params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS,
unit)
if unit == TEMP_FAHRENHEIT:
temp_high = round(temp_high)
if temp_high < min_temp or temp_high > max_temp:
raise SmartHomeError(
@@ -612,7 +623,10 @@ class TemperatureSettingTrait(_Trait):
"{} and {}".format(min_temp, max_temp))
temp_low = temp_util.convert(
params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS, unit)
params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS,
unit)
if unit == TEMP_FAHRENHEIT:
temp_low = round(temp_low)
if temp_low < min_temp or temp_low > max_temp:
raise SmartHomeError(
@@ -712,6 +726,8 @@ class FanSpeedTrait(_Trait):
modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, [])
speeds = []
for mode in modes:
if mode not in self.speed_synonyms:
continue
speed = {
"speed_name": mode,
"speed_values": [{
@@ -750,3 +766,188 @@ class FanSpeedTrait(_Trait):
ATTR_ENTITY_ID: self.state.entity_id,
fan.ATTR_SPEED: params['fanSpeed']
}, blocking=True)
@register_trait
class ModesTrait(_Trait):
"""Trait to set modes.
https://developers.google.com/actions/smarthome/traits/modes
"""
name = TRAIT_MODES
commands = [
COMMAND_MODES
]
# Google requires specific mode names and settings. Here is the full list.
# https://developers.google.com/actions/reference/smarthome/traits/modes
# All settings are mapped here as of 2018-11-28 and can be used for other
# entity types.
HA_TO_GOOGLE = {
media_player.ATTR_INPUT_SOURCE: "input source",
}
SUPPORTED_MODE_SETTINGS = {
'xsmall': [
'xsmall', 'extra small', 'min', 'minimum', 'tiny', 'xs'],
'small': ['small', 'half'],
'large': ['large', 'big', 'full'],
'xlarge': ['extra large', 'xlarge', 'xl'],
'Cool': ['cool', 'rapid cool', 'rapid cooling'],
'Heat': ['heat'], 'Low': ['low'],
'Medium': ['medium', 'med', 'mid', 'half'],
'High': ['high'],
'Auto': ['auto', 'automatic'],
'Bake': ['bake'], 'Roast': ['roast'],
'Convection Bake': ['convection bake', 'convect bake'],
'Convection Roast': ['convection roast', 'convect roast'],
'Favorite': ['favorite'],
'Broil': ['broil'],
'Warm': ['warm'],
'Off': ['off'],
'On': ['on'],
'Normal': [
'normal', 'normal mode', 'normal setting', 'standard',
'schedule', 'original', 'default', 'old settings'
],
'None': ['none'],
'Tap Cold': ['tap cold'],
'Cold Warm': ['cold warm'],
'Hot': ['hot'],
'Extra Hot': ['extra hot'],
'Eco': ['eco'],
'Wool': ['wool', 'fleece'],
'Turbo': ['turbo'],
'Rinse': ['rinse', 'rinsing', 'rinse wash'],
'Away': ['away', 'holiday'],
'maximum': ['maximum'],
'media player': ['media player'],
'chromecast': ['chromecast'],
'tv': [
'tv', 'television', 'tv position', 'television position',
'watching tv', 'watching tv position', 'entertainment',
'entertainment position'
],
'am fm': ['am fm', 'am radio', 'fm radio'],
'internet radio': ['internet radio'],
'satellite': ['satellite'],
'game console': ['game console'],
'antifrost': ['antifrost', 'anti-frost'],
'boost': ['boost'],
'Clock': ['clock'],
'Message': ['message'],
'Messages': ['messages'],
'News': ['news'],
'Disco': ['disco'],
'antifreeze': ['antifreeze', 'anti-freeze', 'anti freeze'],
'balanced': ['balanced', 'normal'],
'swing': ['swing'],
'media': ['media', 'media mode'],
'panic': ['panic'],
'ring': ['ring'],
'frozen': ['frozen', 'rapid frozen', 'rapid freeze'],
'cotton': ['cotton', 'cottons'],
'blend': ['blend', 'mix'],
'baby wash': ['baby wash'],
'synthetics': ['synthetic', 'synthetics', 'compose'],
'hygiene': ['hygiene', 'sterilization'],
'smart': ['smart', 'intelligent', 'intelligence'],
'comfortable': ['comfortable', 'comfort'],
'manual': ['manual'],
'energy saving': ['energy saving'],
'sleep': ['sleep'],
'quick wash': ['quick wash', 'fast wash'],
'cold': ['cold'],
'airsupply': ['airsupply', 'air supply'],
'dehumidification': ['dehumidication', 'dehumidify'],
'game': ['game', 'game mode']
}
@staticmethod
def supported(domain, features):
"""Test if state is supported."""
if domain != media_player.DOMAIN:
return False
return features & media_player.SUPPORT_SELECT_SOURCE
def sync_attributes(self):
"""Return mode attributes for a sync request."""
sources_list = self.state.attributes.get(
media_player.ATTR_INPUT_SOURCE_LIST, [])
modes = []
sources = {}
if sources_list:
sources = {
"name": self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE),
"name_values": [{
"name_synonym": ['input source'],
"lang": "en"
}],
"settings": [],
"ordered": False
}
for source in sources_list:
if source in self.SUPPORTED_MODE_SETTINGS:
src = source
synonyms = self.SUPPORTED_MODE_SETTINGS.get(src)
elif source.lower() in self.SUPPORTED_MODE_SETTINGS:
src = source.lower()
synonyms = self.SUPPORTED_MODE_SETTINGS.get(src)
else:
continue
sources['settings'].append(
{
"setting_name": src,
"setting_values": [{
"setting_synonym": synonyms,
"lang": "en"
}]
}
)
if sources:
modes.append(sources)
payload = {'availableModes': modes}
return payload
def query_attributes(self):
"""Return current modes."""
attrs = self.state.attributes
response = {}
mode_settings = {}
if attrs.get(media_player.ATTR_INPUT_SOURCE_LIST):
mode_settings.update({
media_player.ATTR_INPUT_SOURCE: attrs.get(
media_player.ATTR_INPUT_SOURCE)
})
if mode_settings:
response['on'] = self.state.state != STATE_OFF
response['online'] = True
response['currentModeSettings'] = mode_settings
return response
async def execute(self, command, params):
"""Execute an SetModes command."""
settings = params.get('updateModeSettings')
requested_source = settings.get(
self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE))
if requested_source:
for src in self.state.attributes.get(
media_player.ATTR_INPUT_SOURCE_LIST):
if src.lower() == requested_source.lower():
source = src
await self.hass.services.async_call(
media_player.DOMAIN,
media_player.SERVICE_SELECT_SOURCE, {
ATTR_ENTITY_ID: self.state.entity_id,
media_player.ATTR_INPUT_SOURCE: source
}, blocking=True)

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