Compare commits

..

104 Commits

Author SHA1 Message Date
Paulus Schoutsen
9e21765173 Bumped version to 0.76.0b1 2018-08-13 23:17:30 +02:00
Paulus Schoutsen
c0830f1c20 Deprecate remote.api (#15955) 2018-08-13 23:17:21 +02:00
Martin Hjelmare
985f96662e Upgrade pymysensors to 0.17.0 (#15942) 2018-08-13 23:17:21 +02:00
Paulus Schoutsen
e0229b799d Update frontend to 20180813.0 2018-08-13 23:11:56 +02:00
Paulus Schoutsen
69934a9598 Bumped version to 0.76.0b0 2018-08-11 08:58:52 +02:00
Paulus Schoutsen
f24773933c Update frontend to 20180811.0 2018-08-11 08:58:20 +02:00
Franck Nijhof
e17e080639 ✏️ Corrects typo in code comments (#15923)
`MomematicIP` -> `HomematicIP`
2018-08-11 08:47:41 +02:00
cgtobi
055e35b297 Add RMV public transport sensor (#15814)
* Add new public transport sensor for RMV (Rhein-Main area).

* Add required module.

* Fix naming problem.

* Add unit test.

* Update dependency version to 0.0.5.

* Add new requirements.

* Fix variable name.

* Fix issues pointed out in review.

* Remove unnecessary code.

* Fix linter error.

* Fix config value validation.

* Replace minutes as state by departure timestamp. (see ##14983)

* More work on the timestamp. (see ##14983)

* Revert timestamp work until #14983 gets merged.

* Simplify product validation.

* Remove redundant code.

* Address code change requests.

* Address more code change requests.

* Address even more code change requests.

* Simplify destination check.

* Fix linter problem.

* Bump dependency version to 0.0.7.

* Name variable more explicit.

* Only query once a minute.

* Update test case.

* Fix config validation.

* Remove unneeded import.
2018-08-10 19:35:09 +02:00
Robert Svensson
81604a9326 deCONZ - Add support for sirens (#15896)
* Add support for sirenes

* Too quick...

* Fix test

* Use siren instead of sirene
2018-08-10 19:22:12 +02:00
Paulus Schoutsen
a0e9f9f218 Merge remote-tracking branch 'origin/master' into dev 2018-08-10 18:10:56 +02:00
Paulus Schoutsen
0ab3e7a92a Add IndieAuth 4.2.2 redirect uri at client id (#15911)
* Add IndieAuth 4.2.2 redirect uri at client id

* Fix tests

* Add comment

* Limit to first 10kB of each page
2018-08-10 18:09:42 +02:00
Paulus Schoutsen
9512bb9587 Add and restore context in recorder (#15859) 2018-08-10 18:09:01 +02:00
Adam Mills
da916d7b27 Fix bug in translations upload script (#15922) 2018-08-10 11:35:01 -04:00
clayton craft
b370b6a4e4 Update radiotherm to 1.4.1 (#15910) 2018-08-10 16:10:19 +02:00
Ville Skyttä
1911168855 Misc cleanups (#15907)
* device_tracker.huawei_router: Pylint logging-not-lazy fix

* sensor.irish_rail_transport: Clean up redundant self.info test
2018-08-10 16:09:08 +02:00
Joe Lu
f98629b895 Update August component to use py-august:0.6.0 (#15916) 2018-08-10 07:27:49 +02:00
Ville Skyttä
dc01b17260 Some typing related fixes (#15899)
* Fix FlowManager.async_init handler type

It's not a Callable, but typically a key pointing to one in a dict.

* Mark pip_kwargs return type hint as Any-valued dict

install_package takes other than str args too.
2018-08-09 22:53:12 +02:00
Benoit Louy
ef61c0c3a4 Add PJLink media player platform (#15083)
* add pjlink media player component

* retrieve pjlink device name from projector if name isn't specified in configuration

* update .coveragerc

* fix style

* add missing docstrings

* address PR comments from @MartinHjelmare

* fix code style

* use snake case string for source names

* add missing period at the end of comment string

* rewrite method as function

* revert to use source name provided by projector
2018-08-09 19:58:16 +02:00
mountainsandcode
664eae72d1 Add realtime true/false switch for Waze (#15228) 2018-08-09 16:27:29 +02:00
rafale77
86658f310d Fix for multiple camera switches naming of entity (#14028)
* Fix for multiple camera switches naming of entity

appended camera name to the switch entity name.

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Update amcrest.py

* Add digest authentification

* Update rest_command.py

* Update config.py

* Update rest_command.py

* Update config.py
2018-08-09 15:59:23 +02:00
Mattias Welponer
a29f867908 Add HomematicIP Cloud smoke detector device (#15621)
* Add smoke detector device

* Remove not needed __init__ functions
2018-08-09 14:43:13 +02:00
Paulus Schoutsen
28de2d6f75 Merge pull request #15903 from home-assistant/rc
0.75.3
2018-08-09 14:34:31 +02:00
Paulus Schoutsen
37d98474d5 Bumped version to 0.75.3 2018-08-09 13:41:24 +02:00
Jason Hu
5116f02290 Fix downgrade hassio cannot get refresh_token issue (#15874)
* Fix downgrade hassio issue

* Update __init__.py
2018-08-09 13:38:52 +02:00
Jason Hu
2233d7ca98 Fix downgrade hassio cannot get refresh_token issue (#15874)
* Fix downgrade hassio issue

* Update __init__.py
2018-08-09 13:31:48 +02:00
Jason Hu
f58425dd3c Refactor data entry flow (#15883)
* Refactoring data_entry_flow and config_entry_flow

Move SOURCE_* to config_entries
Change data_entry_flow.FlowManager.async_init() source param default
 to None
Change this first step_id as source or init if source is None
_BaseFlowManagerView pass in SOURCE_USER as default source

* First step of data entry flow decided by _async_create_flow() now

* Lint

* Change helpers.config_entry_flow.DiscoveryFlowHandler default step

* Change FlowManager.async_init source param to context dict param
2018-08-09 13:24:14 +02:00
Fabian Affolter
39d19f2183 Upgrade locationsharinglib to 2.0.11 (#15902) 2018-08-09 13:05:28 +02:00
Paulus Schoutsen
99c4c65f69 Add auth/authorize endpoint (#15887) 2018-08-09 09:27:54 +02:00
Fabian Affolter
61901496ec Upgrade pylast to 2.4.0 (#15886) 2018-08-08 22:32:21 +02:00
Steven Looman
0ab65f1ac5 Follow changes to netdisco, separating DLNA into DLNA_DMS and DLNA_DMR (#15877)
* Follow changes to netdisco, separating DLNA into DLNA_DMS and DLNA_DMR

* No uppercase for names of netdisco discoverables
2018-08-08 11:54:22 +02:00
Fabian Affolter
debdc707e9 Upgrade netdisco to 2.0.0 (#15885) 2018-08-08 11:53:43 +02:00
DubhAd
fcc918a146 Update based upon forum post (#15876)
Based upon [this post](https://community.home-assistant.io/t/device-tracker-ping-on-windows-not-working-solved/61474/3) it looks like we've found why people couldn't get the ping tracker working on Windows.
2018-08-07 18:12:36 +02:00
Fabian Affolter
b6bc0097b8 Upgrade requests_mock to 1.5.2 (#15867) 2018-08-07 16:12:16 +02:00
Fabian Affolter
d556edae31 Upgrade Sphinx to 1.7.6 (#15868) 2018-08-07 16:12:01 +02:00
Fabian Affolter
1fb2ea70c2 Upgrade asynctest to 0.12.2 (#15869) 2018-08-07 16:11:47 +02:00
Ville Skyttä
4cbcb4c3a2 Upgrade pylint to 2.1.1 (#15872) 2018-08-07 16:09:19 +02:00
Paulus Schoutsen
d071df0dec Do not make internet connection during tests (#15858)
* Do not make internet connection

* Small improvement
2018-08-07 09:27:40 +02:00
cdce8p
f09f153014 Fix HomeKit test (#15860)
* Don't raise NotImplementedError during test
2018-08-07 09:26:58 +02:00
Fabian Affolter
1d8678c431 Upgrade pysnmp to 4.4.5 (#15854) 2018-08-07 09:13:01 +02:00
Fabian Affolter
51c30980df Upgrade holidays to 0.9.6 (#15831) 2018-08-07 09:12:09 +02:00
Fabian Affolter
cb20c9b1ea Revert "Upgrade requests_mock to 1.5.2"
This reverts commit a7db2ebbe1.
2018-08-07 09:02:54 +02:00
Fabian Affolter
a7db2ebbe1 Upgrade requests_mock to 1.5.2 2018-08-07 09:01:32 +02:00
Robin
61721478f3 Add facebox auth (#15439)
* Adds auth

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py

* Remove TIMEOUT

* Update test_facebox.py

* fix lint

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Adds check_box_health

* Adds test auth

* Update test_facebox.py

* Update test_facebox.py

* Update test_facebox.py

* Update test_facebox.py

* Ups coverage

* Update test_facebox.py

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Update test_facebox.py

* Update facebox.py

* Update facebox.py

* Update facebox.py
2018-08-07 07:30:36 +02:00
Paulus Schoutsen
47fa928425 Bumped version to 0.76.0.dev0 2018-08-06 13:01:32 +02:00
Paulus Schoutsen
10a7accd00 Merge branch 'master' into dev 2018-08-06 13:01:04 +02:00
Paulus Schoutsen
527585ff9c Merge pull request #15856 from home-assistant/rc
0.75.2
2018-08-06 12:59:54 +02:00
Paulus Schoutsen
2f15a40e97 Bumped version to 0.75.2 2018-08-06 12:38:01 +02:00
Dan Cinnamon
ccef9a3e43 Fix envisalink reconnect (#15832)
* Fix logic for handling connection lost/reconnect

* Fixed line length issue.
2018-08-06 12:37:45 +02:00
Fabian Affolter
479dfd1710 Upgrade voluptuous to 0.11.5 (#15830) 2018-08-06 12:37:45 +02:00
Paulus Schoutsen
34ad4bd32d Fix requirements 2018-08-06 12:37:28 +02:00
psike
6031801206 Fix error when Series missing 'episodeFileCount' or 'episodeCount' (#15824)
* Fix error when Series missing 'episodeFileCount' or 'episodeCount'

* Update sonarr.py

* Update sonarr.py
2018-08-06 12:18:36 +02:00
John Arild Berentsen
9cfe0db3c8 Add different pop 012501 ID (#15838) 2018-08-06 11:10:26 +02:00
Jason Hu
8ef2cfa364 Try to fix coveralls unstable result (#15800)
* Create one tox env for code coverage report

pytest-cov generated report in project root folder, not tox env folder.

* Add cov tox env to travis

* Coveralls seems expecting all build jobs upload

* Only upload coverage after cov env success
2018-08-06 10:51:37 +02:00
Jason Hu
12e69202f8 Change to call_service async_stop non-blocking to allow service call finish (#15803)
* Call later sync_stop to allow service call finish

* Change to use non-blocking service all for restart and stop
2018-08-06 10:25:37 +02:00
ahobsonsayers
e4b2ae29bd Fix bt_home_hub_5 device tracker (#15096)
* Fix bt_home_hub_5 device tracker

Updated BT Home Hub 5 device tracker component to get it working again. The old parsing method of the DNS table has been broken for a while causing the component to fail to get connected devices. A new parsing method has been implemened and fixes all previous issues.

* Moved part of code to a published PyPi library

* Fixed Violations

* Fixed bugs in device tracker

* Moved API Specific Code to PyPi Repository

* Updated to fit requested changes, removed test as it is no longer valid and updated requirement_all.txt

* Update to fit style requirements and remove redundant code

* Removed Unnecessary Comment
2018-08-06 07:38:02 +02:00
Ryan Davies
ac4674fdb0 Add max_gps_accuracy option to Google Maps (#15833)
* Google Maps - Add max_gps_accuracy option

* Remove else statement and add continue
2018-08-06 07:17:21 +02:00
Fabian Affolter
f86702e8ab Upgrade shodan to 1.9.0 (#15839) 2018-08-05 22:48:14 +02:00
mattwing
9a84f8b763 Remove 'volume' from return dict (#15842)
https://github.com/home-assistant/home-assistant/issues/15271

intraday results do not return the volume. See https://www.alphavantage.co/documentation/#intraday
2018-08-05 22:11:51 +02:00
Dan Cinnamon
6a32b9bf87 Fix envisalink reconnect (#15832)
* Fix logic for handling connection lost/reconnect

* Fixed line length issue.
2018-08-05 18:51:23 +02:00
Steven Looman
b152becbe0 Add media_player.dlna_dmr component (#14749)
* Add media_player.dlna_dmr component

* PEP 492

* Move DIDL-template up

* Remove max_volume-override option

* Remove picky_device support

* Use DEFAULT_NAME

* Make supported_features static

* Remove unneeded argument

* Proper module-docstring

* Add http dependency

* Remove additional_configuration options, no longer used

* Change default name to 'DLNA Digital Media Renderer'

* Use python-didl-lite for DIDL-Lite-xml construction/parsing

* Handle NOT_IMPLEMENTED for UPnP state variables RelativeTimePosition and CurrentMediaDuration

* Use UPnP-UDN for unique_id

* Proper handling of upnp events

* Keeping flake8 happy

* Update requirements_all.txt

* Make UDN optional

* Ensure NotifyView is started, before using it

* Only subscribe to services we're interested in

* Don't update state_variables if value has not been changed + minor refactoring

* Improve play_media, follow flow of DLNA more closely

* Hopefully fix ClientOSError problems

* Flake8 fixes

* Keep pylint happy

* Catch errors and report gracefully

* Update async_upnp_client to 0.11.0

* Don't be so noisy

* Define/use constants for HTTP status codes

* Add discovery entry for dlna_dmr

* More robustness with regard to state variable not being set (yet)

* Keep privates hidden

* Handle NOT_IMPLEMENTED for CurrentTrackMetaData state variable

* Fixes in async_upnp_client + renew UPnP subscriptions regularly

* Not too eager

* Refactor duplicate code to _current_transport_actions and improve parsing of actions

* Support RC:1 to RC:3 and AVT:1 to AVT:3

* Moved DLNA-specifics to async_upnp_client.dlna.DmrDevice

* Use our own HTTP server to listen for events.

* More clear and explicit log message for easier troubleshooting

* Follow changes by hass, fixes traceback

* Fix not being able to do next

* Changes after review by @MartinHjelmare

* Linting

* Use homeassistant.util.get_local_ip

* Moved upnp event handling to async_upnp_client

* Keeping pylint happy

* Changes after review by @MartinHjelmare
2018-08-05 14:41:18 +02:00
Fabian Affolter
c41aa12d1d Upgrade youtube_dl to 2018.08.04 (#15837) 2018-08-05 13:29:06 +02:00
Thomas Delaet
8a81ee3b4f Velbus auto-discovery (#13742)
* remove velbus fan and light platforms

these platforms should not be there since they can be created with template components based on switch platform

* use latest version of python-velbus which supports auto-discovery of modules

* fix linting errors

* fix linting errors

* fix linting errors

* address review comments from @MartinHjelmare

* update based on automatic feedback

* fix linting errors

* update dependency

* syntax corrections

* fix lint warning

* split out common functionality in VelbusEntity
use sync methods for loading platforms
support unique_ids so that entities are registred in entity registry

* fix linting errors

* fix linting errors

* fix linting errors

* integrate review comments (common functionality in VelbusEntity class)

* rename DOMAIN import to VELBUS_DOMAIN

* revert change created by requirements script

* regen
2018-08-05 10:47:17 +02:00
fucm
5e1836f3a2 Add support for 2 Tahoma IO awning covers (#15660)
* Add Tahoma io:VerticalExteriorAwningIOComponent and io:HorizontalAwningIOComponent

* Fix position of horizontal awning cover

* Add timestamps for lock time

* Adjust open-close actions for horizontal awning cover

* Fix stop action for io:RollerShutterGenericIOComponent

* Remove redundant information

* Use get for dict lookup
2018-08-05 10:44:57 +02:00
Fabian Affolter
9ea3be4dc1 Upgrade voluptuous to 0.11.5 (#15830) 2018-08-04 17:46:14 -07:00
Martin Hjelmare
bce47eb9a4 Fix frontend requirements after bump (#15829) 2018-08-04 22:35:41 +02:00
Pascal Vizeli
018bd8544c Fix lint with wrong frontend version inside requirements_test_all 2018-08-04 22:26:13 +02:00
Pascal Vizeli
bfb9f2a00b Fix lint with wrong frontend version inside requirements_all 2018-08-04 22:24:17 +02:00
Daniel Høyer Iversen
c7a8f1143c Fix rfxtrx device id matching (#15819)
* Issue #15773

Fix PT2262 devices are incorrectly matched in rfxtrx component

* style
2018-08-04 15:23:57 +02:00
Ville Skyttä
dbe44c076e Upgrade pytest to 3.7.1 and pytest-timeout to 1.3.1 (#15809) 2018-08-04 15:22:37 +02:00
Ville Skyttä
3246b49a45 Upgrade pylint to 2.1.0 (#15811)
* Upgrade pylint to 2.1.0

* Remove no longer needed pylint disables
2018-08-04 15:22:22 +02:00
Paulus Schoutsen
0c7d46927e Bump frontend to 20180804.0 2018-08-04 15:21:11 +02:00
Jason Hu
f6935b5d27 Upgrade voluptuous-serialize to 2.0.0 (#15763)
* Upgrade voluptuous-serialize to 2.0.0

* Change to 2.0.0
2018-08-03 05:23:26 -07:00
Robert Svensson
91e8680fc5 Only report color temp when in the correct color mode (#15791) 2018-08-03 13:56:54 +02:00
Jason Hu
6f2000f5e2 Make sure use_x_forward_for and trusted_proxies must config together (#15804)
* Make sure use_x_forward_for and trusted_proxies must config together

* Fix unit test
2018-08-03 13:52:34 +02:00
Paulus Schoutsen
ee180c51cf Bump frontend to 20180803.0 2018-08-03 13:48:32 +02:00
Conrad Juhl Andersen
b63312ff2e Vacuum component: start_pause to individual start and pause commands. (#15751)
* Add start and pause to StateVacuumDevice, move start_pause to VacuumDevice

* Updated demo vacuum and tests

* Add a few more tests
2018-08-02 19:49:38 -07:00
Paulus Schoutsen
59f8a73676 Return True from Nest setup (#15797) 2018-08-02 16:36:37 -06:00
Jesse Rizzo
affd4e7df3 Add Enphase Envoy component (#15081)
* add enphase envoy component

* Add Enphase Envoy component for energy monitoring

* Fix formatting problems

* Fix formatting errors

* Fix formatting errors

* Fix formatting errors

* Change unit of measurement to W or Wh. Return sensor states as integers

* Fix formatting errors

* Fix formatting errors

* Fix formatting errors

* Move import json to update function

* Fix formatting. Add file to .coveragerc

* Add new component to requirements_all.txt

* Move API call to third party library on PyPi

* Refactor

* Run gen_requirements_all.py

* Minor refactor

* Fix indentation

* Fix indentation
2018-08-02 23:14:43 +02:00
Bryan York
38928c4c0e Fix Min/Max Kelvin color temp attribute for Google (#15697)
* Fix Min/Max Kelvin color temp attribute for Google

Max Kelvin is actually Min Mireds and vice-versa. K = 1000000 / mireds

* Update test_smart_home.py

* Update test_trait.py
2018-08-02 22:09:19 +02:00
Diogo Gomes
48af5116b3 Update pymediaroom to 0.6.4 (#15786)
* Dependency version bump

* bump version
2018-08-02 20:13:48 +02:00
Paulus Schoutsen
eb5f6efb43 Update frontend to 20180802.0 2018-08-02 14:23:40 +02:00
Paulus Schoutsen
7972d6a0c6 Update translations 2018-08-02 13:42:45 +02:00
Aaron Bach
bdea9e1333 Add support for OpenUV binary sensors and sensors (#15769)
* Initial commit

* Adjusted ownership and coverage

* Member-requested changes

* Updated Ozone to a value, not an index

* Verbiage update
2018-08-02 07:42:12 +02:00
Wim Haanstra
2f8d66ef2b RitAssist / FleetGO support (#15780)
* RitAssist / FleetGO support

* Fix lint issue
Add to .coveragerc
2018-08-02 07:01:40 +02:00
Jason Hu
589b23b7e2 Revert "Add support for STATE_AUTO of generic_thermostat (#15678)" (#15783)
This reverts commit 2e5131bb21.
2018-08-01 10:04:41 -07:00
Niklas
2e5131bb21 Add support for STATE_AUTO of generic_thermostat (#15678)
Add support for STATE_AUTO of generic_thermostat
2018-08-01 08:07:27 -07:00
Conrad Juhl Andersen
2ff5b4ce95 Add support for STATES of vacuums (#15573)
* Vacuum: Added support for STATES

* Added debug logging and corrected state order

* typo

* Fix travis error, STATE = STATE for readability

* status -> state

* Changed to Entity instead of ToogleEntity

* Updated some vacuums

* Revert changes

* Revert Changes

* added SUPPORT_STATE

* Woof?

* Implement on/off if STATE not supported

* Moved new state vaccum to Class StateVacuumDevice

* Error: I should go to bed

* Moved around methods for easier reading

* Added StateVacuumDevice demo vacuum

* Added tests for StateVacuumDevice demo vacuum

* Fix styling errors

* Refactored to BaseVaccum

* Vacuum will now go back to dock

* Class BaseVacuum is for internal use only

* return -> await

* return -> await
2018-08-01 05:51:38 -07:00
Robert Svensson
f8a478946e deCONZ - support for power plugs (#15752)
* Initial commit for deCONZ switch support

* Fix hound comment

* Fix martins comment; platforms shouldn't depend on another platform

* Fix existing tests

* New tests

* Clean up unnecessary methods

* Bump requirement to v43

* Added device state attributes to light
2018-08-01 11:03:08 +02:00
Oscar Tin Yiu Lai
623f6c841b Expose internal states and fixed on/off state of Dyson Fans (#15716)
* exposing internal state and fixed onoff state

* fixed styling

* revert file mode changes

* removed self type hints

* better unit test and changed the way to return attributes

* made wolfie happy
2018-07-31 21:38:34 -07:00
Ioan Loosley
0b6f2f5b91 Opensky altitude (#15273)
* Added Altitude to opensky

* decided to take all metadata

* Final Tidy

* More formatting

* moving CONF_ALTITUDE to platform

* Moved CONF_ALTITUDE to platform
2018-07-31 21:45:18 +02:00
Mathieu Velten
3445dc1f00 Update pynetgear to 0.4.1 (bugfixes) (#15768) 2018-07-31 21:40:13 +02:00
Fabian Affolter
a11c2a0bd8 Fix docstrings (#15770) 2018-07-31 21:39:37 +02:00
Diogo Gomes
95da41aa15 This component API has been decomissioned on the 31st of May 2018 by Telstra (#15757)
See #15668
2018-07-31 21:27:43 +02:00
Fabian Affolter
27401f4975 Upgrade Mastodon.py to 1.3.1 (#15766) 2018-07-31 21:17:55 +02:00
Scott Albertson
d902a9f279 Add a "Reviewed by Hound" badge (#15767) 2018-07-31 21:17:33 +02:00
priiduonu
03847e6c41 Round precipitation forecast to 1 decimal place (#15759)
The OWM returns precipitation forecast values as they are submitted to their network. It can lead to values like `0.0025000000000004 mm` which does not make sense and messes up the display card. This PR rounds the value to 1 decimal place.
2018-07-31 19:18:11 +02:00
Fabian Affolter
a4f9602405 Convert wind speed to km/h (fixes #15710) (#15740)
* Convert wind speed to km/h (fixes #15710)

* Round speed
2018-07-31 19:11:29 +02:00
John Arild Berentsen
5f214ffa98 Update pyozw to 0.4.9 (#15758)
* update pyozw to 0.4.8

* add requirements_all.txt

* use 0.4.9
2018-07-31 15:14:14 +01:00
Andrey
8ee3b535ef Add disallow_untyped_calls to mypy check. (#15661)
* Add disallow_untyped_calls to mypy check.

* Fix generator
2018-07-31 15:00:17 +01:00
Andrey Kupreychik
951372491c Fixed NDMS for latest firmware (#15511)
* Fixed NDMS for latest firmware.
Now using telnet instead of Web Interface

* Using external library for NDMS interactions

* updated requirements_all

* renamed `mac` to `device` back

* Using generators for name and attributes fetching
2018-07-31 11:14:49 +02:00
Jason Hu
eeb79476de Decouple login flow view and data entry flow view (#15715) 2018-07-30 21:59:18 -07:00
Aaron Bach
1b2d0e7a6f Better handling of Yi camera being disconnected (#15754)
* Better handling of Yi camera being disconnected

* Handling video processing as well

* Cleanup

* Member-requested changes

* Member-requested changes
2018-07-30 21:56:52 -07:00
Teemu R
3208ad27ac Add kodi unique id based on discovery (#15093)
* kodi add unique id based on discovery

* initialize unique_id to None

* use netdisco-extracted mac_address

* use an uuid instead of mac for real uniqueness

* add missing docstring

* verify that there is no entity already for the given unique id

* whitespace fix
2018-07-30 17:09:38 +02:00
superpuffin
cf87b76b0c Upgrade Adafruit-DHT to 1.3.3 (#15706)
* Change to newer pip package

The package Adafruit_Python_DHT==1.3.2 was broken and would not install, breaking DHT sensor support in Home assistant. It has since been fixed in Adafruit-DHT==1.3.3.

See: https://github.com/adafruit/Adafruit_Python_DHT/issues/99

* Update requirements_all.txt

New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.

* Comment out Adafruit-DHT

Adafruit_Python_DHT changed name to Adafruit-DHT, which still need pyx support breaking our CI, need to be comment out.

* Update requirements_all.txt
2018-07-30 15:15:13 +01:00
141 changed files with 3565 additions and 1282 deletions

View File

@@ -215,6 +215,9 @@ omit =
homeassistant/components/opencv.py
homeassistant/components/*/opencv.py
homeassistant/components/openuv.py
homeassistant/components/*/openuv.py
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
@@ -440,6 +443,7 @@ omit =
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ping.py
homeassistant/components/device_tracker/ritassist.py
homeassistant/components/device_tracker/sky_hub.py
homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py
@@ -509,6 +513,7 @@ omit =
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/denonavr.py
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/dlna_dmr.py
homeassistant/components/media_player/dunehd.py
homeassistant/components/media_player/emby.py
homeassistant/components/media_player/epson.py
@@ -532,6 +537,7 @@ omit =
homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/philips_js.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/pjlink.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rio.py
@@ -632,6 +638,7 @@ omit =
homeassistant/components/sensor/eddystone_temperature.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/emoncms.py
homeassistant/components/sensor/enphase_envoy.py
homeassistant/components/sensor/envirophat.py
homeassistant/components/sensor/etherscan.py
homeassistant/components/sensor/fastdotcom.py

View File

@@ -13,7 +13,8 @@ matrix:
- python: "3.5.3"
env: TOXENV=typing
- python: "3.5.3"
env: TOXENV=py35
env: TOXENV=cov
after_success: coveralls
- python: "3.6"
env: TOXENV=py36
- python: "3.7"
@@ -45,4 +46,3 @@ deploy:
on:
branch: dev
condition: $TOXENV = lint
after_success: coveralls

View File

@@ -98,6 +98,8 @@ homeassistant/components/konnected.py @heythisisnate
homeassistant/components/*/konnected.py @heythisisnate
homeassistant/components/matrix.py @tinloaf
homeassistant/components/*/matrix.py @tinloaf
homeassistant/components/openuv.py @bachya
homeassistant/components/*/openuv.py @bachya
homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/rainmachine/* @bachya

View File

@@ -1,5 +1,5 @@
Home Assistant |Build Status| |Coverage Status| |Chat Status|
=============================================================
Home Assistant |Build Status| |Coverage Status| |Chat Status| |Reviewed by Hound|
=================================================================================
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
@@ -33,6 +33,8 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
:target: https://houndci.com
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
:target: https://home-assistant.io/demo/
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png

View File

@@ -2,9 +2,10 @@
import asyncio
import logging
from collections import OrderedDict
from typing import List, Awaitable
from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from . import models
from . import auth_store
@@ -13,7 +14,9 @@ from .providers import auth_provider_from_config
_LOGGER = logging.getLogger(__name__)
async def auth_manager_from_config(hass, provider_configs):
async def auth_manager_from_config(
hass: HomeAssistant,
provider_configs: List[dict]) -> Awaitable['AuthManager']:
"""Initialize an auth manager from config."""
store = auth_store.AuthStore(hass)
if provider_configs:
@@ -208,7 +211,7 @@ class AuthManager:
return tkn
async def _async_create_login_flow(self, handler, *, source, data):
async def _async_create_login_flow(self, handler, *, context, data):
"""Create a login flow."""
auth_provider = self._providers[handler]

View File

@@ -10,6 +10,7 @@ Component design guidelines:
import asyncio
import itertools as it
import logging
from typing import Awaitable
import homeassistant.core as ha
import homeassistant.config as conf_util
@@ -109,7 +110,7 @@ def async_reload_core_config(hass):
@asyncio.coroutine
def async_setup(hass, config):
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up general services related to Home Assistant."""
@asyncio.coroutine
def async_handle_turn_service(service):

View File

@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
_CONFIGURING = {}
REQUIREMENTS = ['py-august==0.4.0']
REQUIREMENTS = ['py-august==0.6.0']
DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10

View File

@@ -1,6 +1,10 @@
"""Helpers to resolve client ID/secret."""
import asyncio
from html.parser import HTMLParser
from ipaddress import ip_address, ip_network
from urllib.parse import urlparse
from urllib.parse import urlparse, urljoin
from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces
ALLOWED_IPS = (
@@ -16,7 +20,7 @@ ALLOWED_NETWORKS = (
)
def verify_redirect_uri(client_id, redirect_uri):
async def verify_redirect_uri(hass, client_id, redirect_uri):
"""Verify that the client and redirect uri match."""
try:
client_id_parts = _parse_client_id(client_id)
@@ -25,16 +29,75 @@ def verify_redirect_uri(client_id, redirect_uri):
redirect_parts = _parse_url(redirect_uri)
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
# but needs to be specified in link tag when fetching `client_id`.
# This is not implemented.
# Verify redirect url and client url have same scheme and domain.
return (
is_valid = (
client_id_parts.scheme == redirect_parts.scheme and
client_id_parts.netloc == redirect_parts.netloc
)
if is_valid:
return True
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
# but needs to be specified in link tag when fetching `client_id`.
redirect_uris = await fetch_redirect_uris(hass, client_id)
return redirect_uri in redirect_uris
class LinkTagParser(HTMLParser):
"""Parser to find link tags."""
def __init__(self, rel):
"""Initialize a link tag parser."""
super().__init__()
self.rel = rel
self.found = []
def handle_starttag(self, tag, attrs):
"""Handle finding a start tag."""
if tag != 'link':
return
attrs = dict(attrs)
if attrs.get('rel') == self.rel:
self.found.append(attrs.get('href'))
async def fetch_redirect_uris(hass, url):
"""Find link tag with redirect_uri values.
IndieAuth 4.2.2
The client SHOULD publish one or more <link> tags or Link HTTP headers with
a rel attribute of redirect_uri at the client_id URL.
We limit to the first 10kB of the page.
We do not implement extracting redirect uris from headers.
"""
session = hass.helpers.aiohttp_client.async_get_clientsession()
parser = LinkTagParser('redirect_uri')
chunks = 0
try:
resp = await session.get(url, timeout=5)
async for data in resp.content.iter_chunked(1024):
parser.feed(data.decode())
chunks += 1
if chunks == 10:
break
except (asyncio.TimeoutError, ClientError):
pass
# Authorization endpoints verifying that a redirect_uri is allowed for use
# by a client MUST look for an exact match of the given redirect_uri in the
# request against the list of redirect_uris discovered after resolving any
# relative URLs.
return [urljoin(url, found) for found in parser.found]
def verify_client_id(client_id):
"""Verify that the client id is valid."""

View File

@@ -68,8 +68,6 @@ from homeassistant.components.http.ban import process_wrong_login, \
log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from . import indieauth
@@ -97,13 +95,41 @@ class AuthProvidersView(HomeAssistantView):
} for provider in request.app['hass'].auth.auth_providers])
class LoginFlowIndexView(FlowManagerIndexView):
def _prepare_result_json(result):
"""Convert result to JSON."""
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
data = result.copy()
data.pop('result')
data.pop('data')
return data
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
data = result.copy()
schema = data['data_schema']
if schema is None:
data['data_schema'] = []
else:
data['data_schema'] = voluptuous_serialize.convert(schema)
return data
class LoginFlowIndexView(HomeAssistantView):
"""View to create a config flow."""
url = '/auth/login_flow'
name = 'api:auth:login_flow'
requires_auth = False
def __init__(self, flow_mgr):
"""Initialize the flow manager index view."""
self._flow_mgr = flow_mgr
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
@@ -116,15 +142,26 @@ class LoginFlowIndexView(FlowManagerIndexView):
@log_invalid_auth
async def post(self, request, data):
"""Create a new login flow."""
if not indieauth.verify_redirect_uri(data['client_id'],
data['redirect_uri']):
if not await indieauth.verify_redirect_uri(
request.app['hass'], data['client_id'], data['redirect_uri']):
return self.json_message('invalid client id or redirect uri', 400)
# pylint: disable=no-value-for-parameter
return await super().post(request)
if isinstance(data['handler'], list):
handler = tuple(data['handler'])
else:
handler = data['handler']
try:
result = await self._flow_mgr.async_init(handler)
except data_entry_flow.UnknownHandler:
return self.json_message('Invalid handler specified', 404)
except data_entry_flow.UnknownStep:
return self.json_message('Handler does not support init', 400)
return self.json(_prepare_result_json(result))
class LoginFlowResourceView(FlowManagerResourceView):
class LoginFlowResourceView(HomeAssistantView):
"""View to interact with the flow manager."""
url = '/auth/login_flow/{flow_id}'
@@ -133,10 +170,10 @@ class LoginFlowResourceView(FlowManagerResourceView):
def __init__(self, flow_mgr, store_credentials):
"""Initialize the login flow resource view."""
super().__init__(flow_mgr)
self._flow_mgr = flow_mgr
self._store_credentials = store_credentials
async def get(self, request, flow_id):
async def get(self, request):
"""Do not allow getting status of a flow in progress."""
return self.json_message('Invalid flow specified', 404)
@@ -164,9 +201,18 @@ class LoginFlowResourceView(FlowManagerResourceView):
if result['errors'] is not None and \
result['errors'].get('base') == 'invalid_auth':
await process_wrong_login(request)
return self.json(self._prepare_result_json(result))
return self.json(_prepare_result_json(result))
result.pop('data')
result['result'] = self._store_credentials(client_id, result['result'])
return self.json(result)
async def delete(self, request, flow_id):
"""Cancel a flow in progress."""
try:
self._flow_mgr.async_abort(flow_id)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
return self.json_message('Flow aborted')

View File

@@ -122,7 +122,6 @@ class BayesianBinarySensor(BinarySensorDevice):
def async_added_to_hass(self):
"""Call when entity about to be added."""
@callback
# pylint: disable=invalid-name
def async_threshold_sensor_state_listener(entity, old_state,
new_state):
"""Handle sensor state changes."""

View File

@@ -16,10 +16,7 @@ DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
ATTR_WINDOW_STATE = 'window_state'
ATTR_EVENT_DELAY = 'event_delay'
ATTR_MOTION_DETECTED = 'motion_detected'
ATTR_ILLUMINATION = 'illumination'
STATE_SMOKE_OFF = 'IDLE_OFF'
async def async_setup_platform(hass, config, async_add_devices,
@@ -30,15 +27,18 @@ async def async_setup_platform(hass, config, async_add_devices,
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the HomematicIP binary sensor from a config entry."""
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
if isinstance(device, ShutterContact):
if isinstance(device, AsyncShutterContact):
devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, MotionDetectorIndoor):
elif isinstance(device, AsyncMotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device))
elif isinstance(device, AsyncSmokeDetector):
devices.append(HomematicipSmokeDetector(home, device))
if devices:
async_add_devices(devices)
@@ -47,10 +47,6 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP shutter contact."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
@property
def device_class(self):
"""Return the class of this sensor."""
@@ -69,11 +65,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
"""MomematicIP motion detector."""
def __init__(self, home, device):
"""Initialize the shutter contact."""
super().__init__(home, device)
"""HomematicIP motion detector."""
@property
def device_class(self):
@@ -86,3 +78,17 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
if self._device.sabotage:
return True
return self._device.motionDetected
class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP smoke detector."""
@property
def device_class(self):
"""Return the class of this sensor."""
return 'smoke'
@property
def is_on(self):
"""Return true if smoke is detected."""
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF

View File

@@ -0,0 +1,103 @@
"""
This platform provides binary sensors for OpenUV data.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.openuv/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.openuv import (
BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE,
TYPE_PROTECTION_WINDOW, OpenUvEntity)
from homeassistant.util.dt import as_local, parse_datetime, utcnow
DEPENDENCIES = ['openuv']
_LOGGER = logging.getLogger(__name__)
ATTR_PROTECTION_WINDOW_STARTING_TIME = 'start_time'
ATTR_PROTECTION_WINDOW_STARTING_UV = 'start_uv'
ATTR_PROTECTION_WINDOW_ENDING_TIME = 'end_time'
ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv'
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the OpenUV binary sensor platform."""
if discovery_info is None:
return
openuv = hass.data[DOMAIN]
binary_sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
name, icon = BINARY_SENSORS[sensor_type]
binary_sensors.append(
OpenUvBinarySensor(openuv, sensor_type, name, icon))
async_add_devices(binary_sensors, True)
class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
"""Define a binary sensor for OpenUV."""
def __init__(self, openuv, sensor_type, name, icon):
"""Initialize the sensor."""
super().__init__(openuv)
self._icon = icon
self._latitude = openuv.client.latitude
self._longitude = openuv.client.longitude
self._name = name
self._sensor_type = sensor_type
self._state = None
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}_{2}'.format(
self._latitude, self._longitude, self._sensor_type)
@callback
def _update_data(self):
"""Update the state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, TOPIC_UPDATE, self._update_data)
async def async_update(self):
"""Update the state."""
data = self.openuv.data[DATA_PROTECTION_WINDOW]['result']
if self._sensor_type == TYPE_PROTECTION_WINDOW:
self._state = parse_datetime(
data['from_time']) <= utcnow() <= parse_datetime(
data['to_time'])
self._attrs.update({
ATTR_PROTECTION_WINDOW_ENDING_TIME:
as_local(parse_datetime(data['to_time'])),
ATTR_PROTECTION_WINDOW_ENDING_UV: data['to_uv'],
ATTR_PROTECTION_WINDOW_STARTING_UV: data['from_uv'],
ATTR_PROTECTION_WINDOW_STARTING_TIME:
as_local(parse_datetime(data['from_time'])),
})

View File

@@ -86,7 +86,6 @@ class ThresholdSensor(BinarySensorDevice):
self._state = False
self.sensor_value = None
# pylint: disable=invalid-name
@callback
def async_threshold_sensor_state_listener(
entity, old_state, new_state):

View File

@@ -4,93 +4,34 @@ Support for Velbus Binary Sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_DEVICES
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA
from homeassistant.components.velbus import DOMAIN
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
from homeassistant.components.velbus import (
DOMAIN as VELBUS_DOMAIN, VelbusEntity)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
vol.Optional('is_pushbutton'): cv.boolean
}
])
})
DEPENDENCIES = ['velbus']
def setup_platform(hass, config, add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up Velbus binary sensors."""
velbus = hass.data[DOMAIN]
add_devices(VelbusBinarySensor(sensor, velbus)
for sensor in config[CONF_DEVICES])
if discovery_info is None:
return
sensors = []
for sensor in discovery_info:
module = hass.data[VELBUS_DOMAIN].get_module(sensor[0])
channel = sensor[1]
sensors.append(VelbusBinarySensor(module, channel))
async_add_devices(sensors)
class VelbusBinarySensor(BinarySensorDevice):
class VelbusBinarySensor(VelbusEntity, BinarySensorDevice):
"""Representation of a Velbus Binary Sensor."""
def __init__(self, binary_sensor, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = binary_sensor[CONF_NAME]
self._module = binary_sensor['module']
self._channel = binary_sensor['channel']
self._is_pushbutton = 'is_pushbutton' in binary_sensor \
and binary_sensor['is_pushbutton']
self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
yield from self.hass.async_add_job(
self._velbus.subscribe, self._on_message)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.PushButtonStatusMessage):
if message.address == self._module and \
self._channel in message.get_channels():
if self._is_pushbutton:
if self._channel in message.closed:
self._toggle()
else:
pass
else:
self._toggle()
def _toggle(self):
if self._state is True:
self._state = False
else:
self._state = True
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the display name of this sensor."""
return self._name
@property
def is_on(self):
"""Return true if the sensor is on."""
return self._state
return self._module.is_closed(self._channel)

View File

@@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.5']
REQUIREMENTS = ['holidays==0.9.6']
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
@@ -25,9 +25,9 @@ ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy',
'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL',
'NewZealand', 'NZ', 'Northern Ireland',
'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland',
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',

View File

@@ -26,9 +26,6 @@ CONF_PROJECT_DUE_DATE = 'due_date_days'
CONF_PROJECT_LABEL_WHITELIST = 'labels'
CONF_PROJECT_WHITELIST = 'include_projects'
# https://github.com/PyCQA/pylint/pull/2320
# pylint: disable=fixme
# Calendar Platform: Does this calendar event last all day?
ALL_DAY = 'all_day'
# Attribute: All tasks in this project

View File

@@ -57,6 +57,7 @@ class YiCamera(Camera):
self._last_url = None
self._manager = hass.data[DATA_FFMPEG]
self._name = config[CONF_NAME]
self._is_on = True
self.host = config[CONF_HOST]
self.port = config[CONF_PORT]
self.path = config[CONF_PATH]
@@ -68,6 +69,11 @@ class YiCamera(Camera):
"""Camera brand."""
return DEFAULT_BRAND
@property
def is_on(self):
"""Determine whether the camera is on."""
return self._is_on
@property
def name(self):
"""Return the name of this camera."""
@@ -81,7 +87,7 @@ class YiCamera(Camera):
try:
await ftp.connect(self.host)
await ftp.login(self.user, self.passwd)
except StatusCodeError as err:
except (ConnectionRefusedError, StatusCodeError) as err:
raise PlatformNotReady(err)
try:
@@ -101,12 +107,13 @@ class YiCamera(Camera):
return None
await ftp.quit()
self._is_on = True
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
self.user, self.passwd, self.host, self.port, self.path,
latest_dir, videos[-1])
except (ConnectionRefusedError, StatusCodeError) as err:
_LOGGER.error('Error while fetching video: %s', err)
self._is_on = False
return None
async def async_camera_image(self):
@@ -114,7 +121,7 @@ class YiCamera(Camera):
from haffmpeg import ImageFrame, IMAGE_JPEG
url = await self._get_latest_video_url()
if url != self._last_url:
if url and url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
self._last_image = await asyncio.shield(
ffmpeg.get_image(
@@ -130,6 +137,9 @@ class YiCamera(Camera):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
if not self._is_on:
return
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
await stream.open_camera(
self._last_url, extra_cmd=self._extra_arguments)

View File

@@ -1,5 +1,5 @@
"""Component to embed Google Cast."""
from homeassistant import data_entry_flow
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
@@ -15,7 +15,7 @@ async def async_setup(hass, config):
if conf is not None:
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, source=data_entry_flow.SOURCE_IMPORT))
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
return True

View File

@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['radiotherm==1.3']
REQUIREMENTS = ['radiotherm==1.4.1']
_LOGGER = logging.getLogger(__name__)

View File

@@ -7,7 +7,7 @@ from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
REQUIREMENTS = ['voluptuous-serialize==1']
REQUIREMENTS = ['voluptuous-serialize==2.0.0']
@asyncio.coroutine
@@ -96,7 +96,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
return self.json([
flw for flw in hass.config_entries.flow.async_progress()
if flw['source'] != data_entry_flow.SOURCE_USER])
if flw['source'] != config_entries.SOURCE_USER])
class ConfigManagerFlowResourceView(FlowManagerResourceView):

View File

@@ -4,8 +4,10 @@ Support for Tahoma cover - shutters etc.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tahoma/
"""
from datetime import timedelta
import logging
from homeassistant.util.dt import utcnow
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
from homeassistant.components.tahoma import (
DOMAIN as TAHOMA_DOMAIN, TahomaDevice)
@@ -14,6 +16,13 @@ DEPENDENCIES = ['tahoma']
_LOGGER = logging.getLogger(__name__)
ATTR_MEM_POS = 'memorized_position'
ATTR_RSSI_LEVEL = 'rssi_level'
ATTR_LOCK_START_TS = 'lock_start_ts'
ATTR_LOCK_END_TS = 'lock_end_ts'
ATTR_LOCK_LEVEL = 'lock_level'
ATTR_LOCK_ORIG = 'lock_originator'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tahoma covers."""
@@ -27,27 +36,107 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class TahomaCover(TahomaDevice, CoverDevice):
"""Representation a Tahoma Cover."""
def __init__(self, tahoma_device, controller):
"""Initialize the device."""
super().__init__(tahoma_device, controller)
self._closure = 0
# 100 equals open
self._position = 100
self._closed = False
self._rssi_level = None
self._icon = None
# Can be 0 and bigger
self._lock_timer = 0
self._lock_start_ts = None
self._lock_end_ts = None
# Can be 'comfortLevel1', 'comfortLevel2', 'comfortLevel3',
# 'comfortLevel4', 'environmentProtection', 'humanProtection',
# 'userLevel1', 'userLevel2'
self._lock_level = None
# Can be 'LSC', 'SAAC', 'SFC', 'UPS', 'externalGateway', 'localUser',
# 'myself', 'rain', 'security', 'temperature', 'timer', 'user', 'wind'
self._lock_originator = None
def update(self):
"""Update method."""
self.controller.get_states([self.tahoma_device])
# For vertical covers
self._closure = self.tahoma_device.active_states.get(
'core:ClosureState')
# For horizontal covers
if self._closure is None:
self._closure = self.tahoma_device.active_states.get(
'core:DeploymentState')
# For all, if available
if 'core:PriorityLockTimerState' in self.tahoma_device.active_states:
old_lock_timer = self._lock_timer
self._lock_timer = \
self.tahoma_device.active_states['core:PriorityLockTimerState']
# Derive timestamps from _lock_timer, only if not already set or
# something has changed
if self._lock_timer > 0:
_LOGGER.debug("Update %s, lock_timer: %d", self._name,
self._lock_timer)
if self._lock_start_ts is None:
self._lock_start_ts = utcnow()
if self._lock_end_ts is None or \
old_lock_timer != self._lock_timer:
self._lock_end_ts = utcnow() +\
timedelta(seconds=self._lock_timer)
else:
self._lock_start_ts = None
self._lock_end_ts = None
else:
self._lock_timer = 0
self._lock_start_ts = None
self._lock_end_ts = None
self._lock_level = self.tahoma_device.active_states.get(
'io:PriorityLockLevelState')
self._lock_originator = self.tahoma_device.active_states.get(
'io:PriorityLockOriginatorState')
self._rssi_level = self.tahoma_device.active_states.get(
'core:RSSILevelState')
# Define which icon to use
if self._lock_timer > 0:
if self._lock_originator == 'wind':
self._icon = 'mdi:weather-windy'
else:
self._icon = 'mdi:lock-alert'
else:
self._icon = None
# Define current position.
# _position: 0 is closed, 100 is fully open.
# 'core:ClosureState': 100 is closed, 0 is fully open.
if self._closure is not None:
self._position = 100 - self._closure
if self._position <= 5:
self._position = 0
if self._position >= 95:
self._position = 100
self._closed = self._position == 0
else:
self._position = None
if 'core:OpenClosedState' in self.tahoma_device.active_states:
self._closed = \
self.tahoma_device.active_states['core:OpenClosedState']\
== 'closed'
else:
self._closed = False
_LOGGER.debug("Update %s, position: %d", self._name, self._position)
@property
def current_cover_position(self):
"""
Return current position of cover.
0 is closed, 100 is fully open.
"""
try:
position = 100 - \
self.tahoma_device.active_states['core:ClosureState']
if position <= 5:
return 0
if position >= 95:
return 100
return position
except KeyError:
return None
"""Return current position of cover."""
return self._position
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
@@ -56,8 +145,7 @@ class TahomaCover(TahomaDevice, CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
return self.current_cover_position == 0
return self._closed
@property
def device_class(self):
@@ -66,13 +154,47 @@ class TahomaCover(TahomaDevice, CoverDevice):
return 'window'
return None
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attr = {}
super_attr = super().device_state_attributes
if super_attr is not None:
attr.update(super_attr)
if 'core:Memorized1PositionState' in self.tahoma_device.active_states:
attr[ATTR_MEM_POS] = self.tahoma_device.active_states[
'core:Memorized1PositionState']
if self._rssi_level is not None:
attr[ATTR_RSSI_LEVEL] = self._rssi_level
if self._lock_start_ts is not None:
attr[ATTR_LOCK_START_TS] = self._lock_start_ts.isoformat()
if self._lock_end_ts is not None:
attr[ATTR_LOCK_END_TS] = self._lock_end_ts.isoformat()
if self._lock_level is not None:
attr[ATTR_LOCK_LEVEL] = self._lock_level
if self._lock_originator is not None:
attr[ATTR_LOCK_ORIG] = self._lock_originator
return attr
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._icon
def open_cover(self, **kwargs):
"""Open the cover."""
self.apply_action('open')
if self.tahoma_device.type == 'io:HorizontalAwningIOComponent':
self.apply_action('close')
else:
self.apply_action('open')
def close_cover(self, **kwargs):
"""Close the cover."""
self.apply_action('close')
if self.tahoma_device.type == 'io:HorizontalAwningIOComponent':
self.apply_action('open')
else:
self.apply_action('close')
def stop_cover(self, **kwargs):
"""Stop the cover."""
@@ -87,5 +209,10 @@ class TahomaCover(TahomaDevice, CoverDevice):
'rts:ExteriorVenetianBlindRTSComponent',
'rts:BlindRTSComponent'):
self.apply_action('my')
elif self.tahoma_device.type in \
('io:HorizontalAwningIOComponent',
'io:RollerShutterGenericIOComponent',
'io:VerticalExteriorAwningIOComponent'):
self.apply_action('stop')
else:
self.apply_action('stopIdentify')

View File

@@ -6,6 +6,7 @@ https://home-assistant.io/components/deconz/
"""
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_API_KEY, CONF_EVENT, CONF_HOST,
CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
@@ -22,7 +23,7 @@ from .const import (
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==42']
REQUIREMENTS = ['pydeconz==43']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -60,7 +61,9 @@ async def async_setup(hass, config):
deconz_config = config[DOMAIN]
if deconz_config and not configured_hosts(hass):
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data=deconz_config
DOMAIN,
context={'source': config_entries.SOURCE_IMPORT},
data=deconz_config
))
return True
@@ -96,7 +99,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_UNSUB] = []
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, component))

View File

@@ -33,6 +33,10 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
self.bridges = []
self.deconz_config = {}
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a deCONZ config flow start.

View File

@@ -14,3 +14,7 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
ATTR_DARK = 'dark'
ATTR_ON = 'on'
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
SIRENS = ["Warning device"]
SWITCH_TYPES = POWER_PLUGS + SIRENS

View File

@@ -5,24 +5,22 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.bt_home_hub_5/
"""
import logging
import re
import xml.etree.ElementTree as ET
import json
from urllib.parse import unquote
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['bthomehub5-devicelist==0.1.1']
_LOGGER = logging.getLogger(__name__)
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
CONF_DEFAULT_IP = '192.168.1.254'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string
vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string,
})
@@ -38,18 +36,19 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialise the scanner."""
import bthomehub5_devicelist
_LOGGER.info("Initialising BT Home Hub 5")
self.host = config.get(CONF_HOST, '192.168.1.254')
self.host = config[CONF_HOST]
self.last_results = {}
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
# Test the router is accessible
data = _get_homehub_data(self.url)
data = bthomehub5_devicelist.get_devicelist(self.host)
self.success_init = data is not None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
self.update_info()
return (device for device in self.last_results)
@@ -57,71 +56,23 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
"""Return the name of the given device or None if we don't know."""
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
self.update_info()
if not self.last_results:
return None
return self.last_results.get(device)
def _update_info(self):
"""Ensure the information from the BT Home Hub 5 is up to date.
Return boolean if scanning successful.
"""
if not self.success_init:
return False
def update_info(self):
"""Ensure the information from the BT Home Hub 5 is up to date."""
import bthomehub5_devicelist
_LOGGER.info("Scanning")
data = _get_homehub_data(self.url)
data = bthomehub5_devicelist.get_devicelist(self.host)
if not data:
_LOGGER.warning("Error scanning devices")
return False
return
self.last_results = data
return True
def _get_homehub_data(url):
"""Retrieve data from BT Home Hub 5 and return parsed result."""
try:
response = requests.get(url, timeout=5)
except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out")
return
if response.status_code == 200:
return _parse_homehub_response(response.text)
_LOGGER.error("Invalid response from Home Hub: %s", response)
def _parse_homehub_response(data_str):
"""Parse the BT Home Hub 5 data format."""
root = ET.fromstring(data_str)
dirty_json = root.find('known_device_list').get('value')
# Normalise the JavaScript data to JSON.
clean_json = unquote(dirty_json.replace('\'', '\"')
.replace('{', '{\"')
.replace(':\"', '\":\"')
.replace('\",', '\",\"'))
known_devices = [x for x in json.loads(clean_json) if x]
devices = {}
for device in known_devices:
name = device.get('name')
mac = device.get('mac')
if _MAC_REGEX.match(mac) or ',' in mac:
for mac_addr in mac.split(','):
if _MAC_REGEX.match(mac_addr):
devices[mac_addr] = name
else:
devices[mac] = name
return devices

View File

@@ -17,7 +17,7 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
REQUIREMENTS = ['locationsharinglib==2.0.7']
REQUIREMENTS = ['locationsharinglib==2.0.11']
_LOGGER = logging.getLogger(__name__)
@@ -26,18 +26,21 @@ ATTR_FULL_NAME = 'full_name'
ATTR_LAST_SEEN = 'last_seen'
ATTR_NICKNAME = 'nickname'
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_MAX_GPS_ACCURACY, default=100000): vol.Coerce(float),
})
def setup_scanner(hass, config: ConfigType, see, discovery_info=None):
"""Set up the scanner."""
"""Set up the Google Maps Location sharing scanner."""
scanner = GoogleMapsScanner(hass, config, see)
return scanner.success_init
@@ -53,6 +56,7 @@ class GoogleMapsScanner:
self.see = see
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY]
try:
self.service = Service(self.username, self.password,
@@ -76,6 +80,14 @@ class GoogleMapsScanner:
_LOGGER.warning("No location(s) shared with this account")
return
if self.max_gps_accuracy is not None and \
person.accuracy > self.max_gps_accuracy:
_LOGGER.info("Ignoring %s update because expected GPS "
"accuracy %s is not met: %s",
person.nickname, self.max_gps_accuracy,
person.accuracy)
continue
attrs = {
ATTR_ADDRESS: person.address,
ATTR_FULL_NAME: person.full_name,

View File

@@ -85,8 +85,7 @@ class HuaweiDeviceScanner(DeviceScanner):
active_clients = [client for client in data if client.state]
self.last_results = active_clients
# pylint: disable=logging-not-lazy
_LOGGER.debug("Active clients: " + "\n"
_LOGGER.debug("Active clients: %s", "\n"
.join((client.mac + " " + client.name)
for client in active_clients))
return True

View File

@@ -5,18 +5,18 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.keenetic_ndms2/
"""
import logging
from collections import namedtuple
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME
)
REQUIREMENTS = ['ndms2_client==0.0.3']
_LOGGER = logging.getLogger(__name__)
# Interface name to track devices for. Most likely one will not need to
@@ -25,11 +25,13 @@ _LOGGER = logging.getLogger(__name__)
CONF_INTERFACE = 'interface'
DEFAULT_INTERFACE = 'Home'
DEFAULT_PORT = 23
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string,
})
@@ -42,21 +44,22 @@ def get_scanner(_hass, config):
return scanner if scanner.success_init else None
Device = namedtuple('Device', ['mac', 'name'])
class KeeneticNDMS2DeviceScanner(DeviceScanner):
"""This class scans for devices using keenetic NDMS2 web interface."""
def __init__(self, config):
"""Initialize the scanner."""
from ndms2_client import Client, TelnetConnection
self.last_results = []
self._url = 'http://%s/rci/show/ip/arp' % config[CONF_HOST]
self._interface = config[CONF_INTERFACE]
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._client = Client(TelnetConnection(
config.get(CONF_HOST),
config.get(CONF_PORT),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
))
self.success_init = self._update_info()
_LOGGER.info("Scanner initialized")
@@ -69,53 +72,32 @@ class KeeneticNDMS2DeviceScanner(DeviceScanner):
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
filter_named = [result.name for result in self.last_results
if result.mac == device]
name = next((
result.name for result in self.last_results
if result.mac == device), None)
return name
if filter_named:
return filter_named[0]
return None
def get_extra_attributes(self, device):
"""Return the IP of the given device."""
attributes = next((
{'ip': result.ip} for result in self.last_results
if result.mac == device), {})
return attributes
def _update_info(self):
"""Get ARP from keenetic router."""
_LOGGER.info("Fetching...")
_LOGGER.debug("Fetching devices from router...")
last_results = []
# doing a request
from ndms2_client import ConnectionException
try:
from requests.auth import HTTPDigestAuth
res = requests.get(self._url, timeout=10, auth=HTTPDigestAuth(
self._username, self._password
))
except requests.exceptions.Timeout:
_LOGGER.error(
"Connection to the router timed out at URL %s", self._url)
self.last_results = [
dev
for dev in self._client.get_devices()
if dev.interface == self._interface
]
_LOGGER.debug("Successfully fetched data from router")
return True
except ConnectionException:
_LOGGER.error("Error fetching data from router")
return False
if res.status_code != 200:
_LOGGER.error(
"Connection failed with http code %s", res.status_code)
return False
try:
result = res.json()
except ValueError:
# If json decoder could not parse the response
_LOGGER.error("Failed to parse response from router")
return False
# parsing response
for info in result:
if info.get('interface') != self._interface:
continue
mac = info.get('mac')
name = info.get('name')
# No address = no item :)
if mac is None:
continue
last_results.append(Device(mac.upper(), name))
self.last_results = last_results
_LOGGER.info("Request successful")
return True

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL,
CONF_DEVICES, CONF_EXCLUDE)
REQUIREMENTS = ['pynetgear==0.4.0']
REQUIREMENTS = ['pynetgear==0.4.1']
_LOGGER = logging.getLogger(__name__)

View File

@@ -38,7 +38,7 @@ class Host:
self.dev_id = dev_id
self._count = config[CONF_PING_COUNT]
if sys.platform == 'win32':
self._ping_cmd = ['ping', '-n 1', '-w', '1000', self.ip_address]
self._ping_cmd = ['ping', '-n', '1', '-w', '1000', self.ip_address]
else:
self._ping_cmd = ['ping', '-n', '-q', '-c1', '-W1',
self.ip_address]

View File

@@ -0,0 +1,87 @@
"""
Support for RitAssist Platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ritassist/
"""
import logging
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.event import track_utc_time_change
REQUIREMENTS = ['ritassist==0.5']
_LOGGER = logging.getLogger(__name__)
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_INCLUDE = 'include'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_INCLUDE, default=[]):
vol.All(cv.ensure_list, [cv.string])
})
def setup_scanner(hass, config: dict, see, discovery_info=None):
"""Set up the DeviceScanner and check if login is valid."""
scanner = RitAssistDeviceScanner(config, see)
if not scanner.login(hass):
_LOGGER.error('RitAssist authentication failed')
return False
return True
class RitAssistDeviceScanner:
"""Define a scanner for the RitAssist platform."""
def __init__(self, config, see):
"""Initialize RitAssistDeviceScanner."""
from ritassist import API
self._include = config.get(CONF_INCLUDE)
self._see = see
self._api = API(config.get(CONF_CLIENT_ID),
config.get(CONF_CLIENT_SECRET),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
def setup(self, hass):
"""Setup a timer and start gathering devices."""
self._refresh()
track_utc_time_change(hass,
lambda now: self._refresh(),
second=range(0, 60, 30))
def login(self, hass):
"""Perform a login on the RitAssist API."""
if self._api.login():
self.setup(hass)
return True
return False
def _refresh(self) -> None:
"""Refresh device information from the platform."""
try:
devices = self._api.get_devices()
for device in devices:
if (not self._include or
device.license_plate in self._include):
self._see(dev_id=device.plate_as_id,
gps=(device.latitude, device.longitude),
attributes=device.state_attributes,
icon='mdi:car')
except requests.exceptions.ConnectionError:
_LOGGER.error('ConnectionError: Could not connect to RitAssist')

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.4']
REQUIREMENTS = ['pysnmp==4.4.5']
_LOGGER = logging.getLogger(__name__)

View File

@@ -13,7 +13,7 @@ import os
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv
@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.5.0']
REQUIREMENTS = ['netdisco==2.0.0']
DOMAIN = 'discovery'
@@ -85,6 +85,7 @@ SERVICE_HANDLERS = {
'volumio': ('media_player', 'volumio'),
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
'freebox': ('device_tracker', 'freebox'),
'dlna_dmr': ('media_player', 'dlna_dmr'),
}
OPTIONAL_SERVICE_HANDLERS = {
@@ -137,7 +138,7 @@ async def async_setup(hass, config):
if service in CONFIG_ENTRY_HANDLERS:
await hass.config_entries.flow.async_init(
CONFIG_ENTRY_HANDLERS[service],
source=data_entry_flow.SOURCE_DISCOVERY,
context={'source': config_entries.SOURCE_DISCOVERY},
data=info
)
return

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
REQUIREMENTS = ['pyenvisalink==2.2']
REQUIREMENTS = ['pyenvisalink==2.3']
_LOGGER = logging.getLogger(__name__)
@@ -111,20 +111,24 @@ def async_setup(hass, config):
def login_fail_callback(data):
"""Handle when the evl rejects our login."""
_LOGGER.error("The Envisalink rejected your credentials")
sync_connect.set_result(False)
if not sync_connect.done():
sync_connect.set_result(False)
@callback
def connection_fail_callback(data):
"""Network failure callback."""
_LOGGER.error("Could not establish a connection with the Envisalink")
sync_connect.set_result(False)
if not sync_connect.done():
sync_connect.set_result(False)
@callback
def connection_success_callback(data):
"""Handle a successful connection."""
_LOGGER.info("Established a connection with the Envisalink")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
sync_connect.set_result(True)
if not sync_connect.done():
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
stop_envisalink)
sync_connect.set_result(True)
@callback
def zones_updated_callback(data):

View File

@@ -18,6 +18,9 @@ _LOGGER = logging.getLogger(__name__)
CONF_NIGHT_MODE = 'night_mode'
ATTR_IS_NIGHT_MODE = 'is_night_mode'
ATTR_IS_AUTO_MODE = 'is_auto_mode'
DEPENDENCIES = ['dyson']
DYSON_FAN_DEVICES = 'dyson_fan_devices'
@@ -158,7 +161,7 @@ class DysonPureCoolLinkDevice(FanEntity):
def is_on(self):
"""Return true if the entity is on."""
if self._device.state:
return self._device.state.fan_state == "FAN"
return self._device.state.fan_mode == "FAN"
return False
@property
@@ -232,3 +235,11 @@ class DysonPureCoolLinkDevice(FanEntity):
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED
@property
def device_state_attributes(self) -> dict:
"""Return optional state attributes."""
return {
ATTR_IS_NIGHT_MODE: self.is_night_mode,
ATTR_IS_AUTO_MODE: self.is_auto_mode
}

View File

@@ -1,187 +0,0 @@
"""
Support for Velbus platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.fan import (
SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED,
PLATFORM_SCHEMA)
from homeassistant.components.velbus import DOMAIN
from homeassistant.const import CONF_NAME, CONF_DEVICES, STATE_OFF
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel_low'): cv.positive_int,
vol.Required('channel_medium'): cv.positive_int,
vol.Required('channel_high'): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
}
])
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Fans."""
velbus = hass.data[DOMAIN]
add_devices(VelbusFan(fan, velbus) for fan in config[CONF_DEVICES])
class VelbusFan(FanEntity):
"""Representation of a Velbus Fan."""
def __init__(self, fan, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = fan[CONF_NAME]
self._module = fan['module']
self._channel_low = fan['channel_low']
self._channel_medium = fan['channel_medium']
self._channel_high = fan['channel_high']
self._channels = [self._channel_low, self._channel_medium,
self._channel_high]
self._channels_state = [False, False, False]
self._speed = STATE_OFF
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage) and \
message.address == self._module and \
message.channel in self._channels:
if message.channel == self._channel_low:
self._channels_state[0] = message.is_on()
elif message.channel == self._channel_medium:
self._channels_state[1] = message.is_on()
elif message.channel == self._channel_high:
self._channels_state[2] = message.is_on()
self._calculate_speed()
self.schedule_update_ha_state()
def _calculate_speed(self):
if self._is_off():
self._speed = STATE_OFF
elif self._is_low():
self._speed = SPEED_LOW
elif self._is_medium():
self._speed = SPEED_MEDIUM
elif self._is_high():
self._speed = SPEED_HIGH
def _is_off(self):
return self._channels_state[0] is False and \
self._channels_state[1] is False and \
self._channels_state[2] is False
def _is_low(self):
return self._channels_state[0] is True and \
self._channels_state[1] is False and \
self._channels_state[2] is False
def _is_medium(self):
return self._channels_state[0] is True and \
self._channels_state[1] is True and \
self._channels_state[2] is False
def _is_high(self):
return self._channels_state[0] is True and \
self._channels_state[1] is False and \
self._channels_state[2] is True
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def speed(self):
"""Return the current speed."""
return self._speed
@property
def speed_list(self):
"""Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def turn_on(self, speed=None, **kwargs):
"""Turn on the entity."""
if speed is None:
speed = SPEED_MEDIUM
self.set_speed(speed)
def turn_off(self, **kwargs):
"""Turn off the entity."""
self.set_speed(STATE_OFF)
def set_speed(self, speed):
"""Set the speed of the fan."""
channels_off = []
channels_on = []
if speed == STATE_OFF:
channels_off = self._channels
elif speed == SPEED_LOW:
channels_off = [self._channel_medium, self._channel_high]
channels_on = [self._channel_low]
elif speed == SPEED_MEDIUM:
channels_off = [self._channel_high]
channels_on = [self._channel_low, self._channel_medium]
elif speed == SPEED_HIGH:
channels_off = [self._channel_medium]
channels_on = [self._channel_low, self._channel_high]
for channel in channels_off:
self._relay_off(channel)
for channel in channels_on:
self._relay_on(channel)
self.schedule_update_ha_state()
def _relay_on(self, channel):
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [channel]
self._velbus.send(message)
def _relay_off(self, channel):
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [channel]
self._velbus.send(message)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = self._channels
self._velbus.send(message)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_SET_SPEED

View File

@@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180804.0']
REQUIREMENTS = ['home-assistant-frontend==20180813.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
@@ -249,6 +249,7 @@ async def async_setup(hass, config):
index_view = IndexView(repo_path, js_version, hass.auth.active)
hass.http.register_view(index_view)
hass.http.register_view(AuthorizeView(repo_path, js_version))
@callback
def async_finalize_panel(panel):
@@ -334,6 +335,35 @@ def _async_setup_themes(hass, themes):
hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes)
class AuthorizeView(HomeAssistantView):
"""Serve the frontend."""
url = '/auth/authorize'
name = 'auth:authorize'
requires_auth = False
def __init__(self, repo_path, js_option):
"""Initialize the frontend view."""
self.repo_path = repo_path
self.js_option = js_option
async def get(self, request: web.Request):
"""Redirect to the authorize page."""
latest = self.repo_path is not None or \
_is_latest(self.js_option, request)
if latest:
location = '/frontend_latest/authorize.html'
else:
location = '/frontend_es5/authorize.html'
location += '?{}'.format(request.query_string)
return web.Response(status=302, headers={
'location': location
})
class IndexView(HomeAssistantView):
"""Serve the frontend."""

View File

@@ -175,10 +175,13 @@ def async_setup(hass, config):
if data is None:
data = {}
refresh_token = None
if 'hassio_user' in data:
user = yield from hass.auth.async_get_user(data['hassio_user'])
refresh_token = list(user.refresh_tokens.values())[0]
else:
if user:
refresh_token = list(user.refresh_tokens.values())[0]
if refresh_token is None:
user = yield from hass.auth.async_create_system_user('Hass.io')
refresh_token = yield from hass.auth.async_create_refresh_token(user)
data['hassio_user'] = user.id

View File

@@ -10,6 +10,7 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
from .const import (
DOMAIN, HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME,
@@ -41,7 +42,8 @@ async def async_setup(hass, config):
for conf in accesspoints:
if conf[CONF_ACCESSPOINT] not in configured_haps(hass):
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
HMIPC_HAPID: conf[CONF_ACCESSPOINT],
HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN],
HMIPC_NAME: conf[CONF_NAME],

View File

@@ -27,6 +27,10 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
"""Initialize HomematicIP Cloud config flow."""
self.auth = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
errors = {}

View File

@@ -66,8 +66,8 @@ HTTP_SCHEMA = vol.Schema({
vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
vol.Optional(CONF_TRUSTED_PROXIES, default=[]):
vol.Inclusive(CONF_USE_X_FORWARDED_FOR, 'proxy'): cv.boolean,
vol.Inclusive(CONF_TRUSTED_PROXIES, 'proxy'):
vol.All(cv.ensure_list, [ip_network]),
vol.Optional(CONF_TRUSTED_NETWORKS, default=[]):
vol.All(cv.ensure_list, [ip_network]),
@@ -96,8 +96,8 @@ async def async_setup(hass, config):
ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
trusted_proxies = conf[CONF_TRUSTED_PROXIES]
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, [])
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]

View File

@@ -9,7 +9,7 @@ import logging
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant import config_entries
from homeassistant.const import CONF_FILENAME, CONF_HOST
from homeassistant.helpers import aiohttp_client, config_validation as cv
@@ -108,7 +108,8 @@ async def async_setup(hass, config):
# deadlock: creating a config entry will set up the component but the
# setup would block till the entry is created!
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source=data_entry_flow.SOURCE_IMPORT, data={
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
'host': bridge_conf[CONF_HOST],
'path': bridge_conf[CONF_FILENAME],
}

View File

@@ -51,7 +51,8 @@ class HueBridge:
# linking procedure. When linking succeeds, it will remove the
# old config entry.
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
'host': host,
}
))

View File

@@ -50,6 +50,10 @@ class HueFlowHandler(data_entry_flow.FlowHandler):
"""Initialize the Hue flow."""
self.host = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from aiohue.discovery import discover_nupnp

View File

@@ -17,25 +17,29 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE,
CONF_ENTITY_ID, CONF_NAME, DOMAIN)
from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT)
from homeassistant.const import (
CONF_IP_ADDRESS, CONF_PORT, CONF_PASSWORD, CONF_USERNAME,
HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED)
_LOGGER = logging.getLogger(__name__)
ATTR_BOUNDING_BOX = 'bounding_box'
ATTR_CLASSIFIER = 'classifier'
ATTR_IMAGE_ID = 'image_id'
ATTR_ID = 'id'
ATTR_MATCHED = 'matched'
FACEBOX_NAME = 'name'
CLASSIFIER = 'facebox'
DATA_FACEBOX = 'facebox_classifiers'
EVENT_CLASSIFIER_TEACH = 'image_processing.teach_classifier'
FILE_PATH = 'file_path'
SERVICE_TEACH_FACE = 'facebox_teach_face'
TIMEOUT = 9
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
})
SERVICE_TEACH_SCHEMA = vol.Schema({
@@ -45,6 +49,26 @@ SERVICE_TEACH_SCHEMA = vol.Schema({
})
def check_box_health(url, username, password):
"""Check the health of the classifier and return its id if healthy."""
kwargs = {}
if username:
kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password)
try:
response = requests.get(
url,
**kwargs
)
if response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error("AuthenticationError on %s", CLASSIFIER)
return None
if response.status_code == HTTP_OK:
return response.json()['hostname']
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
return None
def encode_image(image):
"""base64 encode an image stream."""
base64_img = base64.b64encode(image).decode('ascii')
@@ -63,10 +87,10 @@ def parse_faces(api_faces):
for entry in api_faces:
face = {}
if entry['matched']: # This data is only in matched faces.
face[ATTR_NAME] = entry['name']
face[FACEBOX_NAME] = entry['name']
face[ATTR_IMAGE_ID] = entry['id']
else: # Lets be explicit.
face[ATTR_NAME] = None
face[FACEBOX_NAME] = None
face[ATTR_IMAGE_ID] = None
face[ATTR_CONFIDENCE] = round(100.0*entry['confidence'], 2)
face[ATTR_MATCHED] = entry['matched']
@@ -75,17 +99,46 @@ def parse_faces(api_faces):
return known_faces
def post_image(url, image):
def post_image(url, image, username, password):
"""Post an image to the classifier."""
kwargs = {}
if username:
kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password)
try:
response = requests.post(
url,
json={"base64": encode_image(image)},
timeout=TIMEOUT
**kwargs
)
if response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error("AuthenticationError on %s", CLASSIFIER)
return None
return response
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
return None
def teach_file(url, name, file_path, username, password):
"""Teach the classifier a name associated with a file."""
kwargs = {}
if username:
kwargs['auth'] = requests.auth.HTTPBasicAuth(username, password)
try:
with open(file_path, 'rb') as open_file:
response = requests.post(
url,
data={FACEBOX_NAME: name, ATTR_ID: file_path},
files={'file': open_file},
**kwargs
)
if response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error("AuthenticationError on %s", CLASSIFIER)
elif response.status_code == HTTP_BAD_REQUEST:
_LOGGER.error("%s teaching of file %s failed with message:%s",
CLASSIFIER, file_path, response.text)
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
def valid_file_path(file_path):
@@ -104,13 +157,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if DATA_FACEBOX not in hass.data:
hass.data[DATA_FACEBOX] = []
ip_address = config[CONF_IP_ADDRESS]
port = config[CONF_PORT]
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
url_health = "http://{}:{}/healthz".format(ip_address, port)
hostname = check_box_health(url_health, username, password)
if hostname is None:
return
entities = []
for camera in config[CONF_SOURCE]:
facebox = FaceClassifyEntity(
config[CONF_IP_ADDRESS],
config[CONF_PORT],
camera[CONF_ENTITY_ID],
camera.get(CONF_NAME))
ip_address, port, username, password, hostname,
camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
entities.append(facebox)
hass.data[DATA_FACEBOX].append(facebox)
add_devices(entities)
@@ -129,33 +189,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
classifier.teach(name, file_path)
hass.services.register(
DOMAIN,
SERVICE_TEACH_FACE,
service_handle,
DOMAIN, SERVICE_TEACH_FACE, service_handle,
schema=SERVICE_TEACH_SCHEMA)
class FaceClassifyEntity(ImageProcessingFaceEntity):
"""Perform a face classification."""
def __init__(self, ip, port, camera_entity, name=None):
def __init__(self, ip_address, port, username, password, hostname,
camera_entity, name=None):
"""Init with the API key and model id."""
super().__init__()
self._url_check = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER)
self._url_teach = "http://{}:{}/{}/teach".format(ip, port, CLASSIFIER)
self._url_check = "http://{}:{}/{}/check".format(
ip_address, port, CLASSIFIER)
self._url_teach = "http://{}:{}/{}/teach".format(
ip_address, port, CLASSIFIER)
self._username = username
self._password = password
self._hostname = hostname
self._camera = camera_entity
if name:
self._name = name
else:
camera_name = split_entity_id(camera_entity)[1]
self._name = "{} {}".format(
CLASSIFIER, camera_name)
self._name = "{} {}".format(CLASSIFIER, camera_name)
self._matched = {}
def process_image(self, image):
"""Process an image."""
response = post_image(self._url_check, image)
if response is not None:
response = post_image(
self._url_check, image, self._username, self._password)
if response:
response_json = response.json()
if response_json['success']:
total_faces = response_json['facesCount']
@@ -173,34 +237,8 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
if (not self.hass.config.is_allowed_path(file_path)
or not valid_file_path(file_path)):
return
with open(file_path, 'rb') as open_file:
response = requests.post(
self._url_teach,
data={ATTR_NAME: name, 'id': file_path},
files={'file': open_file})
if response.status_code == 200:
self.hass.bus.fire(
EVENT_CLASSIFIER_TEACH, {
ATTR_CLASSIFIER: CLASSIFIER,
ATTR_NAME: name,
FILE_PATH: file_path,
'success': True,
'message': None
})
elif response.status_code == 400:
_LOGGER.warning(
"%s teaching of file %s failed with message:%s",
CLASSIFIER, file_path, response.text)
self.hass.bus.fire(
EVENT_CLASSIFIER_TEACH, {
ATTR_CLASSIFIER: CLASSIFIER,
ATTR_NAME: name,
FILE_PATH: file_path,
'success': False,
'message': response.text
})
teach_file(
self._url_teach, name, file_path, self._username, self._password)
@property
def camera_entity(self):
@@ -218,4 +256,5 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
return {
'matched_faces': self._matched,
'total_matched_faces': len(self._matched),
'hostname': self._hostname
}

View File

@@ -4,9 +4,9 @@ Support for deCONZ light.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light.deconz/
"""
from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS
from homeassistant.components.deconz.const import (
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
@@ -32,7 +32,8 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
"""Add light from deCONZ."""
entities = []
for light in lights:
entities.append(DeconzLight(light))
if light.type not in SWITCH_TYPES:
entities.append(DeconzLight(light))
async_add_devices(entities, True)
hass.data[DATA_DECONZ_UNSUB].append(
@@ -189,3 +190,12 @@ class DeconzLight(Light):
del data['on']
await self._light.async_set_state(data)
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
attributes['is_deconz_group'] = self._light.type == 'LightGroup'
if self._light.type == 'LightGroup':
attributes['all_on'] = self._light.all_on
return attributes

View File

@@ -254,8 +254,6 @@ def _mean_tuple(*args):
return tuple(sum(l) / len(l) for l in zip(*args))
# https://github.com/PyCQA/pylint/issues/1831
# pylint: disable=bad-whitespace
def _reduce_attribute(states: List[State],
key: str,
default: Optional[Any] = None,

View File

@@ -1,104 +0,0 @@
"""
Support for Velbus lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_DEVICES
from homeassistant.components.light import Light, PLATFORM_SCHEMA
from homeassistant.components.velbus import DOMAIN
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['velbus']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
{
vol.Required('module'): cv.positive_int,
vol.Required('channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string
}
])
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Lights."""
velbus = hass.data[DOMAIN]
add_devices(VelbusLight(light, velbus) for light in config[CONF_DEVICES])
class VelbusLight(Light):
"""Representation of a Velbus Light."""
def __init__(self, light, velbus):
"""Initialize a Velbus light."""
self._velbus = velbus
self._name = light[CONF_NAME]
self._module = light['module']
self._channel = light['channel']
self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage) and \
message.address == self._module and \
message.channel == self._channel:
self._state = message.is_on()
self.schedule_update_ha_state()
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def is_on(self):
"""Return true if the light is on."""
return self._state
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = [self._channel]
self._velbus.send(message)

View File

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

View File

@@ -0,0 +1,400 @@
# -*- coding: utf-8 -*-
"""
Support for DLNA DMR (Device Media Renderer).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.dlna_dmr/
"""
import asyncio
import functools
import logging
from datetime import datetime
import aiohttp
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MediaPlayerDevice,
PLATFORM_SCHEMA)
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
CONF_URL, CONF_NAME,
STATE_OFF, STATE_ON, STATE_IDLE, STATE_PLAYING, STATE_PAUSED)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import get_local_ip
DLNA_DMR_DATA = 'dlna_dmr'
REQUIREMENTS = [
'async-upnp-client==0.12.2',
]
DEFAULT_NAME = 'DLNA Digital Media Renderer'
DEFAULT_LISTEN_PORT = 8301
CONF_LISTEN_IP = 'listen_ip'
CONF_LISTEN_PORT = 'listen_port'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_LISTEN_IP): cv.string,
vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
HOME_ASSISTANT_UPNP_CLASS_MAPPING = {
'music': 'object.item.audioItem',
'tvshow': 'object.item.videoItem',
'video': 'object.item.videoItem',
'episode': 'object.item.videoItem',
'channel': 'object.item.videoItem',
'playlist': 'object.item.playlist',
}
HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = {
'music': 'audio/*',
'tvshow': 'video/*',
'video': 'video/*',
'episode': 'video/*',
'channel': 'video/*',
'playlist': 'playlist/*',
}
_LOGGER = logging.getLogger(__name__)
def catch_request_errors():
"""Catch asyncio.TimeoutError, aiohttp.ClientError errors."""
def call_wrapper(func):
"""Call wrapper for decorator."""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
"""Catch asyncio.TimeoutError, aiohttp.ClientError errors."""
try:
return func(self, *args, **kwargs)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error during call %s", func.__name__)
return wrapper
return call_wrapper
async def async_start_event_handler(hass, server_host, server_port, requester):
"""Register notify view."""
hass_data = hass.data[DLNA_DMR_DATA]
if 'event_handler' in hass_data:
return hass_data['event_handler']
# start event handler
from async_upnp_client.aiohttp import AiohttpNotifyServer
server = AiohttpNotifyServer(requester,
server_port,
server_host,
hass.loop)
await server.start_server()
_LOGGER.info('UPNP/DLNA event handler listening on: %s',
server.callback_url)
hass_data['notify_server'] = server
hass_data['event_handler'] = server.event_handler
# register for graceful shutdown
async def async_stop_server(event):
"""Stop server."""
_LOGGER.debug('Stopping UPNP/DLNA event handler')
await server.stop_server()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_server)
return hass_data['event_handler']
async def async_setup_platform(hass: HomeAssistant,
config,
async_add_devices,
discovery_info=None):
"""Set up DLNA DMR platform."""
if config.get(CONF_URL) is not None:
url = config[CONF_URL]
name = config.get(CONF_NAME)
elif discovery_info is not None:
url = discovery_info['ssdp_description']
name = discovery_info['name']
if DLNA_DMR_DATA not in hass.data:
hass.data[DLNA_DMR_DATA] = {}
if 'lock' not in hass.data[DLNA_DMR_DATA]:
hass.data[DLNA_DMR_DATA]['lock'] = asyncio.Lock()
# build upnp/aiohttp requester
from async_upnp_client.aiohttp import AiohttpSessionRequester
session = async_get_clientsession(hass)
requester = AiohttpSessionRequester(session, True)
# ensure event handler has been started
with await hass.data[DLNA_DMR_DATA]['lock']:
server_host = config.get(CONF_LISTEN_IP)
if server_host is None:
server_host = get_local_ip()
server_port = config.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT)
event_handler = await async_start_event_handler(hass,
server_host,
server_port,
requester)
# create upnp device
from async_upnp_client import UpnpFactory
factory = UpnpFactory(requester, disable_state_variable_validation=True)
try:
upnp_device = await factory.async_create_device(url)
except (asyncio.TimeoutError, aiohttp.ClientError):
raise PlatformNotReady()
# wrap with DmrDevice
from async_upnp_client.dlna import DmrDevice
dlna_device = DmrDevice(upnp_device, event_handler)
# create our own device
device = DlnaDmrDevice(dlna_device, name)
_LOGGER.debug("Adding device: %s", device)
async_add_devices([device], True)
class DlnaDmrDevice(MediaPlayerDevice):
"""Representation of a DLNA DMR device."""
def __init__(self, dmr_device, name=None):
"""Initializer."""
self._device = dmr_device
self._name = name
self._available = False
self._subscription_renew_time = None
async def async_added_to_hass(self):
"""Callback when added."""
self._device.on_event = self._on_event
# register unsubscribe on stop
bus = self.hass.bus
bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
self._async_on_hass_stop)
@property
def available(self):
"""Device is available."""
return self._available
async def _async_on_hass_stop(self, event):
"""Event handler on HASS stop."""
with await self.hass.data[DLNA_DMR_DATA]['lock']:
await self._device.async_unsubscribe_services()
async def async_update(self):
"""Retrieve the latest data."""
was_available = self._available
try:
await self._device.async_update()
self._available = True
except (asyncio.TimeoutError, aiohttp.ClientError):
self._available = False
_LOGGER.debug("Device unavailable")
return
# do we need to (re-)subscribe?
now = datetime.now()
should_renew = self._subscription_renew_time and \
now >= self._subscription_renew_time
if should_renew or \
not was_available and self._available:
try:
timeout = await self._device.async_subscribe_services()
self._subscription_renew_time = datetime.now() + timeout / 2
except (asyncio.TimeoutError, aiohttp.ClientError):
self._available = False
_LOGGER.debug("Could not (re)subscribe")
def _on_event(self, service, state_variables):
"""State variable(s) changed, let home-assistant know."""
self.schedule_update_ha_state()
@property
def supported_features(self):
"""Flag media player features that are supported."""
supported_features = 0
if self._device.has_volume_level:
supported_features |= SUPPORT_VOLUME_SET
if self._device.has_volume_mute:
supported_features |= SUPPORT_VOLUME_MUTE
if self._device.has_play:
supported_features |= SUPPORT_PLAY
if self._device.has_pause:
supported_features |= SUPPORT_PAUSE
if self._device.has_stop:
supported_features |= SUPPORT_STOP
if self._device.has_previous:
supported_features |= SUPPORT_PREVIOUS_TRACK
if self._device.has_next:
supported_features |= SUPPORT_NEXT_TRACK
if self._device.has_play_media:
supported_features |= SUPPORT_PLAY_MEDIA
return supported_features
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._device.volume_level
@catch_request_errors()
async def async_set_volume_level(self, volume):
"""Set volume level, range 0..1."""
await self._device.async_set_volume_level(volume)
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._device.is_volume_muted
@catch_request_errors()
async def async_mute_volume(self, mute):
"""Mute the volume."""
desired_mute = bool(mute)
await self._device.async_mute_volume(desired_mute)
@catch_request_errors()
async def async_media_pause(self):
"""Send pause command."""
if not self._device.can_pause:
_LOGGER.debug('Cannot do Pause')
return
await self._device.async_pause()
@catch_request_errors()
async def async_media_play(self):
"""Send play command."""
if not self._device.can_play:
_LOGGER.debug('Cannot do Play')
return
await self._device.async_play()
@catch_request_errors()
async def async_media_stop(self):
"""Send stop command."""
if not self._device.can_stop:
_LOGGER.debug('Cannot do Stop')
return
await self._device.async_stop()
@catch_request_errors()
async def async_play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
title = "Home Assistant"
mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING[media_type]
upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING[media_type]
# stop current playing media
if self._device.can_stop:
await self.async_media_stop()
# queue media
await self._device.async_set_transport_uri(media_id,
title,
mime_type,
upnp_class)
await self._device.async_wait_for_can_play()
# if already playing, no need to call Play
from async_upnp_client import dlna
if self._device.state == dlna.STATE_PLAYING:
return
# play it
await self.async_media_play()
@catch_request_errors()
async def async_media_previous_track(self):
"""Send previous track command."""
if not self._device.can_previous:
_LOGGER.debug('Cannot do Previous')
return
await self._device.async_previous()
@catch_request_errors()
async def async_media_next_track(self):
"""Send next track command."""
if not self._device.can_next:
_LOGGER.debug('Cannot do Next')
return
await self._device.async_next()
@property
def media_title(self):
"""Title of current playing media."""
return self._device.media_title
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._device.media_image_url
@property
def state(self):
"""State of the player."""
if not self._available:
return STATE_OFF
from async_upnp_client import dlna
if self._device.state is None:
return STATE_ON
if self._device.state == dlna.STATE_PLAYING:
return STATE_PLAYING
if self._device.state == dlna.STATE_PAUSED:
return STATE_PAUSED
return STATE_IDLE
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._device.media_duration
@property
def media_position(self):
"""Position of current playing media in seconds."""
return self._device.media_position
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return self._device.media_position_updated_at
@property
def name(self) -> str:
"""Return the name of the device."""
if self._name:
return self._name
return self._device.name
@property
def unique_id(self) -> str:
"""Return an unique ID."""
return self._device.udn

View File

@@ -160,6 +160,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = dict()
unique_id = None
# Is this a manual configuration?
if discovery_info is None:
name = config.get(CONF_NAME)
@@ -175,6 +176,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
tcp_port = DEFAULT_TCP_PORT
encryption = DEFAULT_PROXY_SSL
websocket = DEFAULT_ENABLE_WEBSOCKET
properties = discovery_info.get('properties')
if properties is not None:
unique_id = properties.get('uuid', None)
# Only add a device once, so discovered devices do not override manual
# config.
@@ -182,6 +186,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if ip_addr in hass.data[DATA_KODI]:
return
# If we got an unique id, check that it does not exist already.
# This is necessary as netdisco does not deterministally return the same
# advertisement when the service is offered over multiple IP addresses.
if unique_id is not None:
for device in hass.data[DATA_KODI].values():
if device.unique_id == unique_id:
return
entity = KodiDevice(
hass,
name=name,
@@ -190,7 +202,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
password=config.get(CONF_PASSWORD),
turn_on_action=config.get(CONF_TURN_ON_ACTION),
turn_off_action=config.get(CONF_TURN_OFF_ACTION),
timeout=config.get(CONF_TIMEOUT), websocket=websocket)
timeout=config.get(CONF_TIMEOUT), websocket=websocket,
unique_id=unique_id)
hass.data[DATA_KODI][ip_addr] = entity
async_add_devices([entity], update_before_add=True)
@@ -260,12 +273,14 @@ class KodiDevice(MediaPlayerDevice):
def __init__(self, hass, name, host, port, tcp_port, encryption=False,
username=None, password=None,
turn_on_action=None, turn_off_action=None,
timeout=DEFAULT_TIMEOUT, websocket=True):
timeout=DEFAULT_TIMEOUT, websocket=True,
unique_id=None):
"""Initialize the Kodi device."""
import jsonrpc_async
import jsonrpc_websocket
self.hass = hass
self._name = name
self._unique_id = unique_id
kwargs = {
'timeout': timeout,
@@ -384,6 +399,11 @@ class KodiDevice(MediaPlayerDevice):
_LOGGER.debug("Unable to fetch kodi data", exc_info=True)
return None
@property
def unique_id(self):
"""Return the unique id of the device."""
return self._unique_id
@property
def state(self):
"""Return the state of the device."""

View File

@@ -25,7 +25,7 @@ from homeassistant.const import (
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymediaroom==0.6.3']
REQUIREMENTS = ['pymediaroom==0.6.4']
_LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,157 @@
"""
Support for controlling projector via the PJLink protocol.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.pjlink/
"""
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE,
SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_ON, CONF_HOST,
CONF_NAME, CONF_PASSWORD, CONF_PORT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pypjlink2==1.2.0']
_LOGGER = logging.getLogger(__name__)
CONF_ENCODING = 'encoding'
DEFAULT_PORT = 4352
DEFAULT_ENCODING = 'utf-8'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
})
SUPPORT_PJLINK = SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the PJLink platform."""
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
name = config.get(CONF_NAME)
encoding = config.get(CONF_ENCODING)
password = config.get(CONF_PASSWORD)
if 'pjlink' not in hass.data:
hass.data['pjlink'] = {}
hass_data = hass.data['pjlink']
device_label = "{}:{}".format(host, port)
if device_label in hass_data:
return
device = PjLinkDevice(host, port, name, encoding, password)
hass_data[device_label] = device
add_devices([device], True)
def format_input_source(input_source_name, input_source_number):
"""Format input source for display in UI."""
return "{} {}".format(input_source_name, input_source_number)
class PjLinkDevice(MediaPlayerDevice):
"""Representation of a PJLink device."""
def __init__(self, host, port, name, encoding, password):
"""Iinitialize the PJLink device."""
self._host = host
self._port = port
self._name = name
self._password = password
self._encoding = encoding
self._muted = False
self._pwstate = STATE_OFF
self._current_source = None
with self.projector() as projector:
if not self._name:
self._name = projector.get_name()
inputs = projector.get_inputs()
self._source_name_mapping = \
{format_input_source(*x): x for x in inputs}
self._source_list = sorted(self._source_name_mapping.keys())
def projector(self):
"""Create PJLink Projector instance."""
from pypjlink import Projector
projector = Projector.from_address(self._host, self._port,
self._encoding)
projector.authenticate(self._password)
return projector
def update(self):
"""Get the latest state from the device."""
with self.projector() as projector:
pwstate = projector.get_power()
if pwstate == 'off':
self._pwstate = STATE_OFF
else:
self._pwstate = STATE_ON
self._muted = projector.get_mute()[1]
self._current_source = \
format_input_source(*projector.get_input())
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._pwstate
@property
def is_volume_muted(self):
"""Return boolean indicating mute status."""
return self._muted
@property
def source(self):
"""Return current input source."""
return self._current_source
@property
def source_list(self):
"""Return all available input sources."""
return self._source_list
@property
def supported_features(self):
"""Return projector supported features."""
return SUPPORT_PJLINK
def turn_off(self):
"""Turn projector off."""
with self.projector() as projector:
projector.set_power('off')
def turn_on(self):
"""Turn projector on."""
with self.projector() as projector:
projector.set_power('on')
def mute_volume(self, mute):
"""Mute (true) of unmute (false) media player."""
with self.projector() as projector:
from pypjlink import MUTE_AUDIO
projector.set_mute(MUTE_AUDIO, mute)
def select_source(self, source):
"""Set the input source."""
source = self._source_name_mapping[source]
with self.projector() as projector:
projector.set_input(*source)

View File

@@ -22,7 +22,7 @@ from .const import (
from .device import get_mysensors_devices
from .gateway import get_mysensors_gateway, setup_gateways, finish_setup
REQUIREMENTS = ['pymysensors==0.16.0']
REQUIREMENTS = ['pymysensors==0.17.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -11,6 +11,7 @@ from datetime import datetime, timedelta
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS,
CONF_MONITORED_CONDITIONS,
@@ -103,7 +104,8 @@ async def async_setup(hass, config):
access_token_cache_file = hass.config.path(filename)
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
'nest_conf_path': access_token_cache_file,
}
))

View File

@@ -58,6 +58,10 @@ class NestFlowHandler(data_entry_flow.FlowHandler):
"""Initialize the Nest config flow."""
self.flow_impl = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
flows = self.hass.data.get(DATA_FLOW_IMPL, {})

View File

@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_ACCESS_TOKEN
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['Mastodon.py==1.3.0']
REQUIREMENTS = ['Mastodon.py==1.3.1']
_LOGGER = logging.getLogger(__name__)

View File

@@ -1,104 +0,0 @@
"""
Telstra API platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.telstra/
"""
import logging
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
import requests
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONTENT_TYPE_JSON
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_CONSUMER_KEY = 'consumer_key'
CONF_CONSUMER_SECRET = 'consumer_secret'
CONF_PHONE_NUMBER = 'phone_number'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CONSUMER_KEY): cv.string,
vol.Required(CONF_CONSUMER_SECRET): cv.string,
vol.Required(CONF_PHONE_NUMBER): cv.string,
})
def get_service(hass, config, discovery_info=None):
"""Get the Telstra SMS API notification service."""
consumer_key = config.get(CONF_CONSUMER_KEY)
consumer_secret = config.get(CONF_CONSUMER_SECRET)
phone_number = config.get(CONF_PHONE_NUMBER)
if _authenticate(consumer_key, consumer_secret) is False:
_LOGGER.exception("Error obtaining authorization from Telstra API")
return None
return TelstraNotificationService(
consumer_key, consumer_secret, phone_number)
class TelstraNotificationService(BaseNotificationService):
"""Implementation of a notification service for the Telstra SMS API."""
def __init__(self, consumer_key, consumer_secret, phone_number):
"""Initialize the service."""
self._consumer_key = consumer_key
self._consumer_secret = consumer_secret
self._phone_number = phone_number
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
title = kwargs.get(ATTR_TITLE)
# Retrieve authorization first
token_response = _authenticate(
self._consumer_key, self._consumer_secret)
if token_response is False:
_LOGGER.exception("Error obtaining authorization from Telstra API")
return
# Send the SMS
if title:
text = '{} {}'.format(title, message)
else:
text = message
message_data = {
'to': self._phone_number,
'body': text,
}
message_resource = 'https://api.telstra.com/v1/sms/messages'
message_headers = {
CONTENT_TYPE: CONTENT_TYPE_JSON,
AUTHORIZATION: 'Bearer {}'.format(token_response['access_token']),
}
message_response = requests.post(
message_resource, headers=message_headers, json=message_data,
timeout=10)
if message_response.status_code != 202:
_LOGGER.exception("Failed to send SMS. Status code: %d",
message_response.status_code)
def _authenticate(consumer_key, consumer_secret):
"""Authenticate with the Telstra API."""
token_data = {
'client_id': consumer_key,
'client_secret': consumer_secret,
'grant_type': 'client_credentials',
'scope': 'SMS'
}
token_resource = 'https://api.telstra.com/v1/oauth/token'
token_response = requests.get(
token_resource, params=token_data, timeout=10).json()
if 'error' in token_response:
return False
return token_response

View File

@@ -2,9 +2,10 @@
from homeassistant.core import callback
from homeassistant.loader import bind_hass
from .const import STEPS, STEP_USER, DOMAIN
from .const import DOMAIN, STEP_USER, STEPS
DEPENDENCIES = ['http']
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
@@ -21,7 +22,7 @@ def async_is_onboarded(hass):
async def async_setup(hass, config):
"""Set up the onboard component."""
"""Set up the onboarding component."""
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
data = await store.async_load()

View File

@@ -3,21 +3,21 @@ import asyncio
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import callback
from .const import DOMAIN, STEPS, STEP_USER
from .const import DOMAIN, STEP_USER, STEPS
async def async_setup(hass, data, store):
"""Setup onboarding."""
"""Set up the onboarding view."""
hass.http.register_view(OnboardingView(data, store))
hass.http.register_view(UserOnboardingView(data, store))
class OnboardingView(HomeAssistantView):
"""Returns the onboarding status."""
"""Return the onboarding status."""
requires_auth = False
url = '/api/onboarding'

View File

@@ -0,0 +1,182 @@
"""
Support for data from openuv.io.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/openuv/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION,
CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL, CONF_SENSORS)
from homeassistant.helpers import (
aiohttp_client, config_validation as cv, discovery)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
REQUIREMENTS = ['pyopenuv==1.0.1']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'openuv'
DATA_PROTECTION_WINDOW = 'protection_window'
DATA_UV = 'uv'
DEFAULT_ATTRIBUTION = 'Data provided by OpenUV'
DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
NOTIFICATION_ID = 'openuv_notification'
NOTIFICATION_TITLE = 'OpenUV Component Setup'
TOPIC_UPDATE = '{0}_data_update'.format(DOMAIN)
TYPE_CURRENT_OZONE_LEVEL = 'current_ozone_level'
TYPE_CURRENT_UV_INDEX = 'current_uv_index'
TYPE_MAX_UV_INDEX = 'max_uv_index'
TYPE_PROTECTION_WINDOW = 'uv_protection_window'
TYPE_SAFE_EXPOSURE_TIME_1 = 'safe_exposure_time_type_1'
TYPE_SAFE_EXPOSURE_TIME_2 = 'safe_exposure_time_type_2'
TYPE_SAFE_EXPOSURE_TIME_3 = 'safe_exposure_time_type_3'
TYPE_SAFE_EXPOSURE_TIME_4 = 'safe_exposure_time_type_4'
TYPE_SAFE_EXPOSURE_TIME_5 = 'safe_exposure_time_type_5'
TYPE_SAFE_EXPOSURE_TIME_6 = 'safe_exposure_time_type_6'
BINARY_SENSORS = {
TYPE_PROTECTION_WINDOW: ('Protection Window', 'mdi:sunglasses')
}
BINARY_SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)])
})
SENSORS = {
TYPE_CURRENT_OZONE_LEVEL: (
'Current Ozone Level', 'mdi:vector-triangle', 'du'),
TYPE_CURRENT_UV_INDEX: ('Current UV Index', 'mdi:weather-sunny', 'index'),
TYPE_MAX_UV_INDEX: ('Max UV Index', 'mdi:weather-sunny', 'index'),
TYPE_SAFE_EXPOSURE_TIME_1: (
'Skin Type 1 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_2: (
'Skin Type 2 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_3: (
'Skin Type 3 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_4: (
'Skin Type 4 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_5: (
'Skin Type 5 Safe Exposure Time', 'mdi:timer', 'minutes'),
TYPE_SAFE_EXPOSURE_TIME_6: (
'Skin Type 6 Safe Exposure Time', 'mdi:timer', 'minutes'),
}
SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)):
vol.All(cv.ensure_list, [vol.In(SENSORS)])
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_ELEVATION): float,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
cv.time_period,
})
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the OpenUV component."""
from pyopenuv import Client
from pyopenuv.errors import OpenUvError
conf = config[DOMAIN]
api_key = conf[CONF_API_KEY]
elevation = conf.get(CONF_ELEVATION, hass.config.elevation)
latitude = conf.get(CONF_LATITUDE, hass.config.latitude)
longitude = conf.get(CONF_LONGITUDE, hass.config.longitude)
try:
websession = aiohttp_client.async_get_clientsession(hass)
openuv = OpenUV(
Client(
api_key, latitude, longitude, websession, altitude=elevation),
conf[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] +
conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS])
await openuv.async_update()
hass.data[DOMAIN] = openuv
except OpenUvError as err:
_LOGGER.error('An error occurred: %s', str(err))
hass.components.persistent_notification.create(
'Error: {0}<br />'
'You will need to restart hass after fixing.'
''.format(err),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
for component, schema in [
('binary_sensor', conf[CONF_BINARY_SENSORS]),
('sensor', conf[CONF_SENSORS]),
]:
hass.async_create_task(
discovery.async_load_platform(
hass, component, DOMAIN, schema, config))
async def refresh_sensors(event_time):
"""Refresh OpenUV data."""
_LOGGER.debug('Refreshing OpenUV data')
await openuv.async_update()
async_dispatcher_send(hass, TOPIC_UPDATE)
async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL])
return True
class OpenUV:
"""Define a generic OpenUV object."""
def __init__(self, client, monitored_conditions):
"""Initialize."""
self._monitored_conditions = monitored_conditions
self.client = client
self.data = {}
async def async_update(self):
"""Update sensor/binary sensor data."""
if TYPE_PROTECTION_WINDOW in self._monitored_conditions:
data = await self.client.uv_protection_window()
self.data[DATA_PROTECTION_WINDOW] = data
if any(c in self._monitored_conditions for c in SENSORS):
data = await self.client.uv_index()
self.data[DATA_UV] = data
class OpenUvEntity(Entity):
"""Define a generic OpenUV entity."""
def __init__(self, openuv):
"""Initialize."""
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._name = None
self.openuv = openuv
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._attrs
@property
def name(self):
"""Return the name of the entity."""
return self._name

View File

@@ -6,10 +6,11 @@ https://home-assistant.io/components/persistent_notification/
"""
import asyncio
import logging
from typing import Awaitable
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import TemplateError
from homeassistant.loader import bind_hass
from homeassistant.helpers import config_validation as cv
@@ -58,7 +59,8 @@ def dismiss(hass, notification_id):
@callback
@bind_hass
def async_create(hass, message, title=None, notification_id=None):
def async_create(hass: HomeAssistant, message: str, title: str = None,
notification_id: str = None) -> None:
"""Generate a notification."""
data = {
key: value for key, value in [
@@ -68,7 +70,8 @@ def async_create(hass, message, title=None, notification_id=None):
] if value is not None
}
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
@callback
@@ -81,7 +84,7 @@ def async_dismiss(hass, notification_id):
@asyncio.coroutine
def async_setup(hass, config):
def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]:
"""Set up the persistent notification component."""
@callback
def create_service(call):

View File

@@ -114,6 +114,27 @@ def _drop_index(engine, table_name, index_name):
"critical operation.", index_name, table_name)
def _add_columns(engine, table_name, columns_def):
"""Add columns to a table."""
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
columns_def = ['ADD COLUMN {}'.format(col_def) for col_def in columns_def]
try:
engine.execute(text("ALTER TABLE {table} {columns_def}".format(
table=table_name,
columns_def=', '.join(columns_def))))
return
except SQLAlchemyError:
pass
for column_def in columns_def:
engine.execute(text("ALTER TABLE {table} {column_def}".format(
table=table_name,
column_def=column_def)))
def _apply_update(engine, new_version, old_version):
"""Perform operations to bring schema up to date."""
if new_version == 1:
@@ -146,6 +167,19 @@ def _apply_update(engine, new_version, old_version):
elif new_version == 5:
# Create supporting index for States.event_id foreign key
_create_index(engine, "states", "ix_states_event_id")
elif new_version == 6:
_add_columns(engine, "events", [
'context_id CHARACTER(36)',
'context_user_id CHARACTER(36)',
])
_create_index(engine, "events", "ix_events_context_id")
_create_index(engine, "events", "ix_events_context_user_id")
_add_columns(engine, "states", [
'context_id CHARACTER(36)',
'context_user_id CHARACTER(36)',
])
_create_index(engine, "states", "ix_states_context_id")
_create_index(engine, "states", "ix_states_context_user_id")
else:
raise ValueError("No schema migration defined for version {}"
.format(new_version))

View File

@@ -9,14 +9,15 @@ from sqlalchemy import (
from sqlalchemy.ext.declarative import declarative_base
import homeassistant.util.dt as dt_util
from homeassistant.core import Event, EventOrigin, State, split_entity_id
from homeassistant.core import (
Context, Event, EventOrigin, State, split_entity_id)
from homeassistant.remote import JSONEncoder
# SQLAlchemy Schema
# pylint: disable=invalid-name
Base = declarative_base()
SCHEMA_VERSION = 5
SCHEMA_VERSION = 6
_LOGGER = logging.getLogger(__name__)
@@ -31,6 +32,8 @@ class Events(Base): # type: ignore
origin = Column(String(32))
time_fired = Column(DateTime(timezone=True), index=True)
created = Column(DateTime(timezone=True), default=datetime.utcnow)
context_id = Column(String(36), index=True)
context_user_id = Column(String(36), index=True)
@staticmethod
def from_event(event):
@@ -38,16 +41,23 @@ class Events(Base): # type: ignore
return Events(event_type=event.event_type,
event_data=json.dumps(event.data, cls=JSONEncoder),
origin=str(event.origin),
time_fired=event.time_fired)
time_fired=event.time_fired,
context_id=event.context.id,
context_user_id=event.context.user_id)
def to_native(self):
"""Convert to a natve HA Event."""
context = Context(
id=self.context_id,
user_id=self.context_user_id
)
try:
return Event(
self.event_type,
json.loads(self.event_data),
EventOrigin(self.origin),
_process_timestamp(self.time_fired)
_process_timestamp(self.time_fired),
context=context,
)
except ValueError:
# When json.loads fails
@@ -69,6 +79,8 @@ class States(Base): # type: ignore
last_updated = Column(DateTime(timezone=True), default=datetime.utcnow,
index=True)
created = Column(DateTime(timezone=True), default=datetime.utcnow)
context_id = Column(String(36), index=True)
context_user_id = Column(String(36), index=True)
__table_args__ = (
# Used for fetching the state of entities at a specific time
@@ -82,7 +94,11 @@ class States(Base): # type: ignore
entity_id = event.data['entity_id']
state = event.data.get('new_state')
dbstate = States(entity_id=entity_id)
dbstate = States(
entity_id=entity_id,
context_id=event.context.id,
context_user_id=event.context.user_id,
)
# State got deleted
if state is None:
@@ -103,12 +119,17 @@ class States(Base): # type: ignore
def to_native(self):
"""Convert to an HA state object."""
context = Context(
id=self.context_id,
user_id=self.context_user_id
)
try:
return State(
self.entity_id, self.state,
json.loads(self.attributes),
_process_timestamp(self.last_changed),
_process_timestamp(self.last_updated)
_process_timestamp(self.last_updated),
context=context,
)
except ValueError:
# When json.loads fails

View File

@@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__)
ATTR_CLOSE = 'close'
ATTR_HIGH = 'high'
ATTR_LOW = 'low'
ATTR_VOLUME = 'volume'
CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage"
CONF_FOREIGN_EXCHANGE = 'foreign_exchange'
@@ -148,7 +147,6 @@ class AlphaVantageSensor(Entity):
ATTR_CLOSE: self.values['4. close'],
ATTR_HIGH: self.values['2. high'],
ATTR_LOW: self.values['3. low'],
ATTR_VOLUME: self.values['5. volume'],
}
@property

View File

@@ -0,0 +1,107 @@
"""
Support for Enphase Envoy solar energy monitor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.enphase_envoy/
"""
import logging
import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS)
REQUIREMENTS = ['envoy_reader==0.1']
_LOGGER = logging.getLogger(__name__)
SENSORS = {
"production": ("Envoy Current Energy Production", 'W'),
"daily_production": ("Envoy Today's Energy Production", "Wh"),
"7_days_production": ("Envoy Last Seven Days Energy Production", "Wh"),
"lifetime_production": ("Envoy Lifetime Energy Production", "Wh"),
"consumption": ("Envoy Current Energy Consumption", "W"),
"daily_consumption": ("Envoy Today's Energy Consumption", "Wh"),
"7_days_consumption": ("Envoy Last Seven Days Energy Consumption", "Wh"),
"lifetime_consumption": ("Envoy Lifetime Energy Consumption", "Wh")
}
ICON = 'mdi:flash'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)):
vol.All(cv.ensure_list, [vol.In(list(SENSORS))])})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Enphase Envoy sensor."""
ip_address = config[CONF_IP_ADDRESS]
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
# Iterate through the list of sensors
for condition in monitored_conditions:
add_devices([Envoy(ip_address, condition, SENSORS[condition][0],
SENSORS[condition][1])], True)
class Envoy(Entity):
"""Implementation of the Enphase Envoy sensors."""
def __init__(self, ip_address, sensor_type, name, unit):
"""Initialize the sensor."""
self._ip_address = ip_address
self._name = name
self._unit_of_measurement = unit
self._type = sensor_type
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
def update(self):
"""Get the energy production data from the Enphase Envoy."""
import envoy_reader
if self._type == "production":
self._state = int(envoy_reader.production(self._ip_address))
elif self._type == "daily_production":
self._state = int(envoy_reader.daily_production(self._ip_address))
elif self._type == "7_days_production":
self._state = int(envoy_reader.seven_days_production(
self._ip_address))
elif self._type == "lifetime_production":
self._state = int(envoy_reader.lifetime_production(
self._ip_address))
elif self._type == "consumption":
self._state = int(envoy_reader.consumption(self._ip_address))
elif self._type == "daily_consumption":
self._state = int(envoy_reader.daily_consumption(
self._ip_address))
elif self._type == "7_days_consumption":
self._state = int(envoy_reader.seven_days_consumption(
self._ip_address))
elif self._type == "lifetime_consumption":
self._state = int(envoy_reader.lifetime_consumption(
self._ip_address))

View File

@@ -164,7 +164,7 @@ class IrishRailTransportData:
ATTR_TRAIN_TYPE: train.get('type')}
self.info.append(train_data)
if not self.info or not self.info:
if not self.info:
self.info = self._empty_train_data()
def _empty_train_data(self):

View File

@@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pylast==2.3.0']
REQUIREMENTS = ['pylast==2.4.0']
ATTR_LAST_PLAYED = 'last_played'
ATTR_PLAY_COUNT = 'play_count'

View File

@@ -124,7 +124,6 @@ class MinMaxSensor(Entity):
self.states = {}
@callback
# pylint: disable=invalid-name
def async_min_max_sensor_state_listener(entity, old_state, new_state):
"""Handle the sensor state changes."""
if new_state.state is None or new_state.state in STATE_UNKNOWN:

View File

@@ -13,22 +13,27 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS,
ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE,
LENGTH_KILOMETERS, LENGTH_METERS)
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE,
CONF_RADIUS, ATTR_ATTRIBUTION, ATTR_LATITUDE,
ATTR_LONGITUDE, LENGTH_KILOMETERS, LENGTH_METERS)
from homeassistant.helpers.entity import Entity
from homeassistant.util import distance as util_distance
from homeassistant.util import location as util_location
_LOGGER = logging.getLogger(__name__)
CONF_ALTITUDE = 'altitude'
ATTR_CALLSIGN = 'callsign'
ATTR_ALTITUDE = 'altitude'
ATTR_ON_GROUND = 'on_ground'
ATTR_SENSOR = 'sensor'
ATTR_STATES = 'states'
DOMAIN = 'opensky'
DEFAULT_ALTITUDE = 0
EVENT_OPENSKY_ENTRY = '{}_entry'.format(DOMAIN)
EVENT_OPENSKY_EXIT = '{}_exit'.format(DOMAIN)
SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds
@@ -38,7 +43,7 @@ OPENSKY_ATTRIBUTION = "Information provided by the OpenSky Network "\
OPENSKY_API_URL = 'https://opensky-network.org/api/states/all'
OPENSKY_API_FIELDS = [
'icao24', ATTR_CALLSIGN, 'origin_country', 'time_position',
'time_velocity', ATTR_LONGITUDE, ATTR_LATITUDE, 'altitude',
'time_velocity', ATTR_LONGITUDE, ATTR_LATITUDE, ATTR_ALTITUDE,
ATTR_ON_GROUND, 'velocity', 'heading', 'vertical_rate', 'sensors']
@@ -46,7 +51,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RADIUS): vol.Coerce(float),
vol.Optional(CONF_NAME): cv.string,
vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude
vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude,
vol.Optional(CONF_ALTITUDE, default=DEFAULT_ALTITUDE): vol.Coerce(float)
})
@@ -56,19 +62,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
add_devices([OpenSkySensor(
hass, config.get(CONF_NAME, DOMAIN), latitude, longitude,
config.get(CONF_RADIUS))], True)
config.get(CONF_RADIUS), config.get(CONF_ALTITUDE))], True)
class OpenSkySensor(Entity):
"""Open Sky Network Sensor."""
def __init__(self, hass, name, latitude, longitude, radius):
def __init__(self, hass, name, latitude, longitude, radius, altitude):
"""Initialize the sensor."""
self._session = requests.Session()
self._latitude = latitude
self._longitude = longitude
self._radius = util_distance.convert(
radius, LENGTH_KILOMETERS, LENGTH_METERS)
self._altitude = altitude
self._state = 0
self._hass = hass
self._name = name
@@ -84,11 +91,18 @@ class OpenSkySensor(Entity):
"""Return the state of the sensor."""
return self._state
def _handle_boundary(self, callsigns, event):
def _handle_boundary(self, flights, event, metadata):
"""Handle flights crossing region boundary."""
for callsign in callsigns:
for flight in flights:
if flight in metadata:
altitude = metadata[flight].get(ATTR_ALTITUDE)
else:
# Assume Flight has landed if missing.
altitude = 0
data = {
ATTR_CALLSIGN: callsign,
ATTR_CALLSIGN: flight,
ATTR_ALTITUDE: altitude,
ATTR_SENSOR: self._name,
}
self._hass.bus.fire(event, data)
@@ -96,30 +110,37 @@ class OpenSkySensor(Entity):
def update(self):
"""Update device state."""
currently_tracked = set()
flight_metadata = {}
states = self._session.get(OPENSKY_API_URL).json().get(ATTR_STATES)
for state in states:
data = dict(zip(OPENSKY_API_FIELDS, state))
flight = dict(zip(OPENSKY_API_FIELDS, state))
callsign = flight[ATTR_CALLSIGN].strip()
if callsign != '':
flight_metadata[callsign] = flight
else:
continue
missing_location = (
data.get(ATTR_LONGITUDE) is None or
data.get(ATTR_LATITUDE) is None)
flight.get(ATTR_LONGITUDE) is None or
flight.get(ATTR_LATITUDE) is None)
if missing_location:
continue
if data.get(ATTR_ON_GROUND):
if flight.get(ATTR_ON_GROUND):
continue
distance = util_location.distance(
self._latitude, self._longitude,
data.get(ATTR_LATITUDE), data.get(ATTR_LONGITUDE))
flight.get(ATTR_LATITUDE), flight.get(ATTR_LONGITUDE))
if distance is None or distance > self._radius:
continue
callsign = data[ATTR_CALLSIGN].strip()
if callsign == '':
altitude = flight.get(ATTR_ALTITUDE)
if altitude > self._altitude and self._altitude != 0:
continue
currently_tracked.add(callsign)
if self._previously_tracked is not None:
entries = currently_tracked - self._previously_tracked
exits = self._previously_tracked - currently_tracked
self._handle_boundary(entries, EVENT_OPENSKY_ENTRY)
self._handle_boundary(exits, EVENT_OPENSKY_EXIT)
self._handle_boundary(entries, EVENT_OPENSKY_ENTRY,
flight_metadata)
self._handle_boundary(exits, EVENT_OPENSKY_EXIT, flight_metadata)
self._state = len(currently_tracked)
self._previously_tracked = currently_tracked

View File

@@ -0,0 +1,121 @@
"""
This platform provides sensors for OpenUV data.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.openuv/
"""
import logging
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.openuv import (
DATA_UV, DOMAIN, SENSORS, TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL,
TYPE_CURRENT_UV_INDEX, TYPE_MAX_UV_INDEX, TYPE_SAFE_EXPOSURE_TIME_1,
TYPE_SAFE_EXPOSURE_TIME_2, TYPE_SAFE_EXPOSURE_TIME_3,
TYPE_SAFE_EXPOSURE_TIME_4, TYPE_SAFE_EXPOSURE_TIME_5,
TYPE_SAFE_EXPOSURE_TIME_6, OpenUvEntity)
from homeassistant.util.dt import as_local, parse_datetime
DEPENDENCIES = ['openuv']
_LOGGER = logging.getLogger(__name__)
ATTR_MAX_UV_TIME = 'time'
EXPOSURE_TYPE_MAP = {
TYPE_SAFE_EXPOSURE_TIME_1: 'st1',
TYPE_SAFE_EXPOSURE_TIME_2: 'st2',
TYPE_SAFE_EXPOSURE_TIME_3: 'st3',
TYPE_SAFE_EXPOSURE_TIME_4: 'st4',
TYPE_SAFE_EXPOSURE_TIME_5: 'st5',
TYPE_SAFE_EXPOSURE_TIME_6: 'st6'
}
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the OpenUV binary sensor platform."""
if discovery_info is None:
return
openuv = hass.data[DOMAIN]
sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
name, icon, unit = SENSORS[sensor_type]
sensors.append(OpenUvSensor(openuv, sensor_type, name, icon, unit))
async_add_devices(sensors, True)
class OpenUvSensor(OpenUvEntity):
"""Define a binary sensor for OpenUV."""
def __init__(self, openuv, sensor_type, name, icon, unit):
"""Initialize the sensor."""
super().__init__(openuv)
self._icon = icon
self._latitude = openuv.client.latitude
self._longitude = openuv.client.longitude
self._name = name
self._sensor_type = sensor_type
self._state = None
self._unit = unit
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def state(self):
"""Return the status of the sensor."""
return self._state
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}_{2}'.format(
self._latitude, self._longitude, self._sensor_type)
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit
@callback
def _update_data(self):
"""Update the state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._update_data)
async def async_update(self):
"""Update the state."""
data = self.openuv.data[DATA_UV]['result']
if self._sensor_type == TYPE_CURRENT_OZONE_LEVEL:
self._state = data['ozone']
elif self._sensor_type == TYPE_CURRENT_UV_INDEX:
self._state = data['uv']
elif self._sensor_type == TYPE_MAX_UV_INDEX:
self._state = data['uv_max']
self._attrs.update({
ATTR_MAX_UV_TIME: as_local(
parse_datetime(data['uv_max_time']))
})
elif self._sensor_type in (TYPE_SAFE_EXPOSURE_TIME_1,
TYPE_SAFE_EXPOSURE_TIME_2,
TYPE_SAFE_EXPOSURE_TIME_3,
TYPE_SAFE_EXPOSURE_TIME_4,
TYPE_SAFE_EXPOSURE_TIME_5,
TYPE_SAFE_EXPOSURE_TIME_6):
self._state = data['safe_exposure_time'][EXPOSURE_TYPE_MAP[
self._sensor_type]]

View File

@@ -0,0 +1,202 @@
"""
Support for real-time departure information for Rhein-Main public transport.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.rmvtransport/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION)
REQUIREMENTS = ['PyRMVtransport==0.0.7']
_LOGGER = logging.getLogger(__name__)
CONF_NEXT_DEPARTURE = 'next_departure'
CONF_STATION = 'station'
CONF_DESTINATIONS = 'destinations'
CONF_DIRECTIONS = 'directions'
CONF_LINES = 'lines'
CONF_PRODUCTS = 'products'
CONF_TIME_OFFSET = 'time_offset'
CONF_MAX_JOURNEYS = 'max_journeys'
DEFAULT_NAME = 'RMV Journey'
VALID_PRODUCTS = ['U-Bahn', 'Tram', 'Bus', 'S', 'RB', 'RE', 'EC', 'IC', 'ICE']
ICONS = {
'U-Bahn': 'mdi:subway',
'Tram': 'mdi:tram',
'Bus': 'mdi:bus',
'S': 'mdi:train',
'RB': 'mdi:train',
'RE': 'mdi:train',
'EC': 'mdi:train',
'IC': 'mdi:train',
'ICE': 'mdi:train',
'SEV': 'mdi:checkbox-blank-circle-outline',
None: 'mdi:clock'
}
ATTRIBUTION = "Data provided by opendata.rmv.de"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NEXT_DEPARTURE): [{
vol.Required(CONF_STATION): cv.string,
vol.Optional(CONF_DESTINATIONS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_DIRECTIONS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_LINES, default=[]):
vol.All(cv.ensure_list, [cv.positive_int, cv.string]),
vol.Optional(CONF_PRODUCTS, default=VALID_PRODUCTS):
vol.All(cv.ensure_list, [vol.In(VALID_PRODUCTS)]),
vol.Optional(CONF_TIME_OFFSET, default=0): cv.positive_int,
vol.Optional(CONF_MAX_JOURNEYS, default=5): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}]
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the RMV departure sensor."""
sensors = []
for next_departure in config.get(CONF_NEXT_DEPARTURE):
sensors.append(
RMVDepartureSensor(
next_departure[CONF_STATION],
next_departure.get(CONF_DESTINATIONS),
next_departure.get(CONF_DIRECTIONS),
next_departure.get(CONF_LINES),
next_departure.get(CONF_PRODUCTS),
next_departure.get(CONF_TIME_OFFSET),
next_departure.get(CONF_MAX_JOURNEYS),
next_departure.get(CONF_NAME)))
add_entities(sensors, True)
class RMVDepartureSensor(Entity):
"""Implementation of an RMV departure sensor."""
def __init__(self, station, destinations, directions,
lines, products, time_offset, max_journeys, name):
"""Initialize the sensor."""
self._station = station
self._name = name
self._state = None
self.data = RMVDepartureData(station, destinations, directions, lines,
products, time_offset, max_journeys)
self._icon = ICONS[None]
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def available(self):
"""Return True if entity is available."""
return self._state is not None
@property
def state(self):
"""Return the next departure time."""
return self._state
@property
def state_attributes(self):
"""Return the state attributes."""
try:
return {
'next_departures': [val for val in self.data.departures[1:]],
'direction': self.data.departures[0].get('direction'),
'line': self.data.departures[0].get('line'),
'minutes': self.data.departures[0].get('minutes'),
'departure_time':
self.data.departures[0].get('departure_time'),
'product': self.data.departures[0].get('product'),
}
except IndexError:
return {}
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self._icon
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return "min"
def update(self):
"""Get the latest data and update the state."""
self.data.update()
if not self.data.departures:
self._state = None
self._icon = ICONS[None]
return
if self._name == DEFAULT_NAME:
self._name = self.data.station
self._station = self.data.station
self._state = self.data.departures[0].get('minutes')
self._icon = ICONS[self.data.departures[0].get('product')]
class RMVDepartureData:
"""Pull data from the opendata.rmv.de web page."""
def __init__(self, station_id, destinations, directions,
lines, products, time_offset, max_journeys):
"""Initialize the sensor."""
import RMVtransport
self.station = None
self._station_id = station_id
self._destinations = destinations
self._directions = directions
self._lines = lines
self._products = products
self._time_offset = time_offset
self._max_journeys = max_journeys
self.rmv = RMVtransport.RMVtransport()
self.departures = []
def update(self):
"""Update the connection data."""
try:
_data = self.rmv.get_departures(self._station_id,
products=self._products,
maxJourneys=50)
except ValueError:
self.departures = []
_LOGGER.warning("Returned data not understood")
return
self.station = _data.get('station')
_deps = []
for journey in _data['journeys']:
# find the first departure meeting the criteria
_nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION}
if self._destinations:
dest_found = False
for dest in self._destinations:
if dest in journey['stops']:
dest_found = True
_nextdep['destination'] = dest
if not dest_found:
continue
elif self._lines and journey['number'] not in self._lines:
continue
elif journey['minutes'] < self._time_offset:
continue
for attr in ['direction', 'departure_time', 'product', 'minutes']:
_nextdep[attr] = journey.get(attr, '')
_nextdep['line'] = journey.get('number', '')
_deps.append(_nextdep)
if len(_deps) > self._max_journeys:
break
self.departures = _deps

View File

@@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['shodan==1.8.1']
REQUIREMENTS = ['shodan==1.9.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -16,7 +16,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN,
CONF_VALUE_TEMPLATE)
REQUIREMENTS = ['pysnmp==4.4.4']
REQUIREMENTS = ['pysnmp==4.4.5']
_LOGGER = logging.getLogger(__name__)

View File

@@ -158,8 +158,12 @@ class SonarrSensor(Entity):
)
elif self.type == 'series':
for show in self.data:
attributes[show['title']] = '{}/{} Episodes'.format(
show['episodeFileCount'], show['episodeCount'])
if 'episodeFileCount' not in show \
or 'episodeCount' not in show:
attributes[show['title']] = 'N/A'
else:
attributes[show['title']] = '{}/{} Episodes'.format(
show['episodeFileCount'], show['episodeCount'])
elif self.type == 'status':
attributes = self.data
return attributes

View File

@@ -97,7 +97,6 @@ class StatisticsSensor(Entity):
hass.async_add_job(self._initialize_from_database)
@callback
# pylint: disable=invalid-name
def async_stats_sensor_state_listener(entity, old_state, new_state):
"""Handle the sensor state changes."""
self._unit_of_measurement = new_state.attributes.get(

View File

@@ -31,8 +31,10 @@ CONF_DESTINATION = 'destination'
CONF_ORIGIN = 'origin'
CONF_INCL_FILTER = 'incl_filter'
CONF_EXCL_FILTER = 'excl_filter'
CONF_REALTIME = 'realtime'
DEFAULT_NAME = 'Waze Travel Time'
DEFAULT_REALTIME = True
ICON = 'mdi:car'
@@ -49,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_INCL_FILTER): cv.string,
vol.Optional(CONF_EXCL_FILTER): cv.string,
vol.Optional(CONF_REALTIME, default=DEFAULT_REALTIME): cv.boolean,
})
@@ -60,9 +63,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
region = config.get(CONF_REGION)
incl_filter = config.get(CONF_INCL_FILTER)
excl_filter = config.get(CONF_EXCL_FILTER)
realtime = config.get(CONF_REALTIME)
sensor = WazeTravelTime(name, origin, destination, region,
incl_filter, excl_filter)
incl_filter, excl_filter, realtime)
add_devices([sensor])
@@ -80,12 +84,13 @@ class WazeTravelTime(Entity):
"""Representation of a Waze travel time sensor."""
def __init__(self, name, origin, destination, region,
incl_filter, excl_filter):
incl_filter, excl_filter, realtime):
"""Initialize the Waze travel time sensor."""
self._name = name
self._region = region
self._incl_filter = incl_filter
self._excl_filter = excl_filter
self._realtime = realtime
self._state = None
self._origin_entity_id = None
self._destination_entity_id = None
@@ -197,7 +202,7 @@ class WazeTravelTime(Entity):
try:
params = WazeRouteCalculator.WazeRouteCalculator(
self._origin, self._destination, self._region)
routes = params.calc_all_routes_info()
routes = params.calc_all_routes_info(real_time=self._realtime)
if self._incl_filter is not None:
routes = {k: v for k, v in routes.items() if

View File

@@ -1,5 +1,5 @@
"""Component to embed Sonos."""
from homeassistant import data_entry_flow
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
@@ -15,7 +15,7 @@ async def async_setup(hass, config):
if conf is not None:
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, source=data_entry_flow.SOURCE_IMPORT))
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
return True

View File

@@ -30,7 +30,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
all_switches = []
for setting in switches:
all_switches.append(AmcrestSwitch(setting, camera))
all_switches.append(AmcrestSwitch(setting, camera, name))
async_add_devices(all_switches, True)
@@ -38,11 +38,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AmcrestSwitch(ToggleEntity):
"""Representation of an Amcrest IP camera switch."""
def __init__(self, setting, camera):
def __init__(self, setting, camera, name):
"""Initialize the Amcrest switch."""
self._setting = setting
self._camera = camera
self._name = SWITCHES[setting][0]
self._name = '{} {}'.format(SWITCHES[setting][0], name)
self._icon = SWITCHES[setting][1]
self._state = None

View File

@@ -0,0 +1,118 @@
"""
Support for deCONZ switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.deconz/
"""
from homeassistant.components.deconz.const import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB,
POWER_PLUGS, SIRENS)
from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Old way of setting up deCONZ switches."""
pass
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up switches for deCONZ component.
Switches are based same device class as lights in deCONZ.
"""
@callback
def async_add_switch(lights):
"""Add switch from deCONZ."""
entities = []
for light in lights:
if light.type in POWER_PLUGS:
entities.append(DeconzPowerPlug(light))
elif light.type in SIRENS:
entities.append(DeconzSiren(light))
async_add_devices(entities, True)
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch))
async_add_switch(hass.data[DATA_DECONZ].lights.values())
class DeconzSwitch(SwitchDevice):
"""Representation of a deCONZ switch."""
def __init__(self, switch):
"""Set up switch and add update callback to get data from websocket."""
self._switch = switch
async def async_added_to_hass(self):
"""Subscribe to switches events."""
self._switch.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._switch.deconz_id
@callback
def async_update_callback(self, reason):
"""Update the switch's state."""
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the name of the switch."""
return self._switch.name
@property
def unique_id(self):
"""Return a unique identifier for this switch."""
return self._switch.uniqueid
@property
def available(self):
"""Return True if light is available."""
return self._switch.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
class DeconzPowerPlug(DeconzSwitch):
"""Representation of power plugs from deCONZ."""
@property
def is_on(self):
"""Return true if switch is on."""
return self._switch.state
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
data = {'on': True}
await self._switch.async_set_state(data)
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
data = {'on': False}
await self._switch.async_set_state(data)
class DeconzSiren(DeconzSwitch):
"""Representation of sirens from deCONZ."""
@property
def is_on(self):
"""Return true if switch is on."""
return self._switch.alert == 'lselect'
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
data = {'alert': 'lselect'}
await self._switch.async_set_state(data)
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
data = {'alert': 'none'}
await self._switch.async_set_state(data)

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pysnmp==4.4.4']
REQUIREMENTS = ['pysnmp==4.4.5']
_LOGGER = logging.getLogger(__name__)

View File

@@ -4,108 +4,42 @@ Support for Velbus switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.velbus/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_DEVICES
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
from homeassistant.components.velbus import DOMAIN
import homeassistant.helpers.config_validation as cv
from homeassistant.components.switch import SwitchDevice
from homeassistant.components.velbus import (
DOMAIN as VELBUS_DOMAIN, VelbusEntity)
_LOGGER = logging.getLogger(__name__)
SWITCH_SCHEMA = {
vol.Required('module'): cv.positive_int,
vol.Required('channel'): cv.positive_int,
vol.Required(CONF_NAME): cv.string
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES):
vol.All(cv.ensure_list, [SWITCH_SCHEMA])
})
DEPENDENCIES = ['velbus']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Switch."""
velbus = hass.data[DOMAIN]
devices = []
for switch in config[CONF_DEVICES]:
devices.append(VelbusSwitch(switch, velbus))
add_devices(devices)
return True
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Velbus Switch platform."""
if discovery_info is None:
return
switches = []
for switch in discovery_info:
module = hass.data[VELBUS_DOMAIN].get_module(switch[0])
channel = switch[1]
switches.append(VelbusSwitch(module, channel))
async_add_devices(switches)
class VelbusSwitch(SwitchDevice):
class VelbusSwitch(VelbusEntity, SwitchDevice):
"""Representation of a switch."""
def __init__(self, switch, velbus):
"""Initialize a Velbus switch."""
self._velbus = velbus
self._name = switch[CONF_NAME]
self._module = switch['module']
self._channel = switch['channel']
self._state = False
@asyncio.coroutine
def async_added_to_hass(self):
"""Add listener for Velbus messages on bus."""
def _init_velbus():
"""Initialize Velbus on startup."""
self._velbus.subscribe(self._on_message)
self.get_status()
yield from self.hass.async_add_job(_init_velbus)
def _on_message(self, message):
import velbus
if isinstance(message, velbus.RelayStatusMessage) and \
message.address == self._module and \
message.channel == self._channel:
self._state = message.is_on()
self.schedule_update_ha_state()
@property
def name(self):
"""Return the display name of this switch."""
return self._name
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def is_on(self):
"""Return true if the switch is on."""
return self._state
return self._module.is_on(self._channel)
def turn_on(self, **kwargs):
"""Instruct the switch to turn on."""
import velbus
message = velbus.SwitchRelayOnMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
self._module.turn_on(self._channel)
def turn_off(self, **kwargs):
"""Instruct the switch to turn off."""
import velbus
message = velbus.SwitchRelayOffMessage()
message.set_defaults(self._module)
message.relay_channels = [self._channel]
self._velbus.send(message)
def get_status(self):
"""Retrieve current status."""
import velbus
message = velbus.ModuleStatusRequestMessage()
message.set_defaults(self._module)
message.channels = [self._channel]
self._velbus.send(message)
self._module.turn_off(self._channel)

View File

@@ -50,6 +50,8 @@ TAHOMA_TYPES = {
'io:WindowOpenerVeluxIOComponent': 'cover',
'io:LightIOSystemSensor': 'sensor',
'rts:GarageDoor4TRTSComponent': 'switch',
'io:VerticalExteriorAwningIOComponent': 'cover',
'io:HorizontalAwningIOComponent': 'cover',
'rtds:RTDSSmokeSensor': 'smoke',
}

View File

@@ -14,12 +14,12 @@ import voluptuous as vol
from homeassistant.components import group
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE,
SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON)
SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_PAUSED, STATE_IDLE)
from homeassistant.loader import bind_hass
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity import (ToggleEntity, Entity)
from homeassistant.helpers.icon import icon_for_battery_level
_LOGGER = logging.getLogger(__name__)
@@ -45,6 +45,8 @@ SERVICE_RETURN_TO_BASE = 'return_to_base'
SERVICE_SEND_COMMAND = 'send_command'
SERVICE_SET_FAN_SPEED = 'set_fan_speed'
SERVICE_START_PAUSE = 'start_pause'
SERVICE_START = 'start'
SERVICE_PAUSE = 'pause'
SERVICE_STOP = 'stop'
VACUUM_SERVICE_SCHEMA = vol.Schema({
@@ -65,6 +67,8 @@ SERVICE_TO_METHOD = {
SERVICE_TURN_OFF: {'method': 'async_turn_off'},
SERVICE_TOGGLE: {'method': 'async_toggle'},
SERVICE_START_PAUSE: {'method': 'async_start_pause'},
SERVICE_START: {'method': 'async_start'},
SERVICE_PAUSE: {'method': 'async_pause'},
SERVICE_RETURN_TO_BASE: {'method': 'async_return_to_base'},
SERVICE_CLEAN_SPOT: {'method': 'async_clean_spot'},
SERVICE_LOCATE: {'method': 'async_locate'},
@@ -75,6 +79,13 @@ SERVICE_TO_METHOD = {
'schema': VACUUM_SEND_COMMAND_SERVICE_SCHEMA},
}
STATE_CLEANING = 'cleaning'
STATE_DOCKED = 'docked'
STATE_IDLE = STATE_IDLE
STATE_PAUSED = STATE_PAUSED
STATE_RETURNING = 'returning'
STATE_ERROR = 'error'
DEFAULT_NAME = 'Vacuum cleaner robot'
SUPPORT_TURN_ON = 1
@@ -89,6 +100,8 @@ SUPPORT_SEND_COMMAND = 256
SUPPORT_LOCATE = 512
SUPPORT_CLEAN_SPOT = 1024
SUPPORT_MAP = 2048
SUPPORT_STATE = 4096
SUPPORT_START = 8192
@bind_hass
@@ -147,6 +160,20 @@ def start_pause(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_START_PAUSE, data)
@bind_hass
def start(hass, entity_id=None):
"""Tell all or specified vacuum to start or resume the current task."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_START, data)
@bind_hass
def pause(hass, entity_id=None):
"""Tell all or the specified vacuum to pause the current task."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_PAUSE, data)
@bind_hass
def stop(hass, entity_id=None):
"""Stop all or specified vacuum."""
@@ -208,33 +235,22 @@ def async_setup(hass, config):
return True
class VacuumDevice(ToggleEntity):
"""Representation of a vacuum cleaner robot."""
class _BaseVacuum(Entity):
"""Representation of a base vacuum.
Contains common properties and functions for all vacuum devices.
"""
@property
def supported_features(self):
"""Flag vacuum cleaner features that are supported."""
raise NotImplementedError()
@property
def status(self):
"""Return the status of the vacuum cleaner."""
return None
@property
def battery_level(self):
"""Return the battery level of the vacuum cleaner."""
return None
@property
def battery_icon(self):
"""Return the battery icon for the vacuum cleaner."""
charging = False
if self.status is not None:
charging = 'charg' in self.status.lower()
return icon_for_battery_level(
battery_level=self.battery_level, charging=charging)
@property
def fan_speed(self):
"""Return the fan speed of the vacuum cleaner."""
@@ -245,6 +261,94 @@ class VacuumDevice(ToggleEntity):
"""Get the list of available fan speed steps of the vacuum cleaner."""
raise NotImplementedError()
def stop(self, **kwargs):
"""Stop the vacuum cleaner."""
raise NotImplementedError()
async def async_stop(self, **kwargs):
"""Stop the vacuum cleaner.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(partial(self.stop, **kwargs))
def return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
raise NotImplementedError()
async def async_return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(
partial(self.return_to_base, **kwargs))
def clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
raise NotImplementedError()
async def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(
partial(self.clean_spot, **kwargs))
def locate(self, **kwargs):
"""Locate the vacuum cleaner."""
raise NotImplementedError()
async def async_locate(self, **kwargs):
"""Locate the vacuum cleaner.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(partial(self.locate, **kwargs))
def set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed."""
raise NotImplementedError()
async def async_set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(
partial(self.set_fan_speed, fan_speed, **kwargs))
def send_command(self, command, params=None, **kwargs):
"""Send a command to a vacuum cleaner."""
raise NotImplementedError()
async def async_send_command(self, command, params=None, **kwargs):
"""Send a command to a vacuum cleaner.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(
partial(self.send_command, command, params=params, **kwargs))
class VacuumDevice(_BaseVacuum, ToggleEntity):
"""Representation of a vacuum cleaner robot."""
@property
def status(self):
"""Return the status of the vacuum cleaner."""
return None
@property
def battery_icon(self):
"""Return the battery icon for the vacuum cleaner."""
charging = False
if self.status is not None:
charging = 'charg' in self.status.lower()
return icon_for_battery_level(
battery_level=self.battery_level, charging=charging)
@property
def state_attributes(self):
"""Return the state attributes of the vacuum cleaner."""
@@ -267,100 +371,88 @@ class VacuumDevice(ToggleEntity):
"""Turn the vacuum on and start cleaning."""
raise NotImplementedError()
def async_turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the vacuum on and start cleaning.
This method must be run in the event loop and returns a coroutine.
This method must be run in the event loop.
"""
return self.hass.async_add_job(partial(self.turn_on, **kwargs))
await self.hass.async_add_executor_job(
partial(self.turn_on, **kwargs))
def turn_off(self, **kwargs):
"""Turn the vacuum off stopping the cleaning and returning home."""
raise NotImplementedError()
def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the vacuum off stopping the cleaning and returning home.
This method must be run in the event loop and returns a coroutine.
This method must be run in the event loop.
"""
return self.hass.async_add_job(partial(self.turn_off, **kwargs))
def return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
raise NotImplementedError()
def async_return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(partial(self.return_to_base, **kwargs))
def stop(self, **kwargs):
"""Stop the vacuum cleaner."""
raise NotImplementedError()
def async_stop(self, **kwargs):
"""Stop the vacuum cleaner.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(partial(self.stop, **kwargs))
def clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
raise NotImplementedError()
def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(partial(self.clean_spot, **kwargs))
def locate(self, **kwargs):
"""Locate the vacuum cleaner."""
raise NotImplementedError()
def async_locate(self, **kwargs):
"""Locate the vacuum cleaner.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(partial(self.locate, **kwargs))
def set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed."""
raise NotImplementedError()
def async_set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(
partial(self.set_fan_speed, fan_speed, **kwargs))
await self.hass.async_add_executor_job(
partial(self.turn_off, **kwargs))
def start_pause(self, **kwargs):
"""Start, pause or resume the cleaning task."""
raise NotImplementedError()
def async_start_pause(self, **kwargs):
async def async_start_pause(self, **kwargs):
"""Start, pause or resume the cleaning task.
This method must be run in the event loop and returns a coroutine.
This method must be run in the event loop.
"""
return self.hass.async_add_job(
await self.hass.async_add_executor_job(
partial(self.start_pause, **kwargs))
def send_command(self, command, params=None, **kwargs):
"""Send a command to a vacuum cleaner."""
class StateVacuumDevice(_BaseVacuum):
"""Representation of a vacuum cleaner robot that supports states."""
@property
def state(self):
"""Return the state of the vacuum cleaner."""
return None
@property
def battery_icon(self):
"""Return the battery icon for the vacuum cleaner."""
charging = bool(self.state == STATE_DOCKED)
return icon_for_battery_level(
battery_level=self.battery_level, charging=charging)
@property
def state_attributes(self):
"""Return the state attributes of the vacuum cleaner."""
data = {}
if self.battery_level is not None:
data[ATTR_BATTERY_LEVEL] = self.battery_level
data[ATTR_BATTERY_ICON] = self.battery_icon
if self.fan_speed is not None:
data[ATTR_FAN_SPEED] = self.fan_speed
data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list
return data
def start(self):
"""Start or resume the cleaning task."""
raise NotImplementedError()
def async_send_command(self, command, params=None, **kwargs):
"""Send a command to a vacuum cleaner.
async def async_start(self):
"""Start or resume the cleaning task.
This method must be run in the event loop and returns a coroutine.
This method must be run in the event loop.
"""
return self.hass.async_add_job(
partial(self.send_command, command, params=params, **kwargs))
await self.hass.async_add_executor_job(self.start)
def pause(self):
"""Pause the cleaning task."""
raise NotImplementedError()
async def async_pause(self):
"""Pause the cleaning task.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(self.pause)

View File

@@ -10,7 +10,9 @@ from homeassistant.components.vacuum import (
ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT,
SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME,
SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, VacuumDevice)
SUPPORT_TURN_ON, SUPPORT_STATE, SUPPORT_START, STATE_CLEANING,
STATE_DOCKED, STATE_IDLE, STATE_PAUSED, STATE_RETURNING, VacuumDevice,
StateVacuumDevice)
_LOGGER = logging.getLogger(__name__)
@@ -28,12 +30,17 @@ SUPPORT_ALL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_BATTERY | \
SUPPORT_CLEAN_SPOT
SUPPORT_STATE_SERVICES = SUPPORT_STATE | SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \
SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT | SUPPORT_START
FAN_SPEEDS = ['min', 'medium', 'high', 'max']
DEMO_VACUUM_COMPLETE = '0_Ground_floor'
DEMO_VACUUM_MOST = '1_First_floor'
DEMO_VACUUM_BASIC = '2_Second_floor'
DEMO_VACUUM_MINIMAL = '3_Third_floor'
DEMO_VACUUM_NONE = '4_Fourth_floor'
DEMO_VACUUM_STATE = '5_Fifth_floor'
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -44,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES),
DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES),
DemoVacuum(DEMO_VACUUM_NONE, 0),
StateDemoVacuum(DEMO_VACUUM_STATE),
])
@@ -204,3 +212,125 @@ class DemoVacuum(VacuumDevice):
self._status = 'Executing {}({})'.format(command, params)
self._state = True
self.schedule_update_ha_state()
class StateDemoVacuum(StateVacuumDevice):
"""Representation of a demo vacuum supporting states."""
def __init__(self, name):
"""Initialize the vacuum."""
self._name = name
self._supported_features = SUPPORT_STATE_SERVICES
self._state = STATE_DOCKED
self._fan_speed = FAN_SPEEDS[1]
self._cleaned_area = 0
self._battery_level = 100
@property
def name(self):
"""Return the name of the vacuum."""
return self._name
@property
def should_poll(self):
"""No polling needed for a demo vacuum."""
return False
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
@property
def state(self):
"""Return the current state of the vacuum."""
return self._state
@property
def battery_level(self):
"""Return the current battery level of the vacuum."""
if self.supported_features & SUPPORT_BATTERY == 0:
return
return max(0, min(100, self._battery_level))
@property
def fan_speed(self):
"""Return the current fan speed of the vacuum."""
if self.supported_features & SUPPORT_FAN_SPEED == 0:
return
return self._fan_speed
@property
def fan_speed_list(self):
"""Return the list of supported fan speeds."""
if self.supported_features & SUPPORT_FAN_SPEED == 0:
return
return FAN_SPEEDS
@property
def device_state_attributes(self):
"""Return device state attributes."""
return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)}
def start(self):
"""Start or resume the cleaning task."""
if self.supported_features & SUPPORT_START == 0:
return
if self._state != STATE_CLEANING:
self._state = STATE_CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def pause(self):
"""Pause the cleaning task."""
if self.supported_features & SUPPORT_PAUSE == 0:
return
if self._state == STATE_CLEANING:
self._state = STATE_PAUSED
self.schedule_update_ha_state()
def stop(self, **kwargs):
"""Stop the cleaning task, do not return to dock."""
if self.supported_features & SUPPORT_STOP == 0:
return
self._state = STATE_IDLE
self.schedule_update_ha_state()
def return_to_base(self, **kwargs):
"""Return dock to charging base."""
if self.supported_features & SUPPORT_RETURN_HOME == 0:
return
self._state = STATE_RETURNING
self.schedule_update_ha_state()
self.hass.loop.call_later(30, self.__set_state_to_dock)
def clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
if self.supported_features & SUPPORT_CLEAN_SPOT == 0:
return
self._state = STATE_CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def set_fan_speed(self, fan_speed, **kwargs):
"""Set the vacuum's fan speed."""
if self.supported_features & SUPPORT_FAN_SPEED == 0:
return
if fan_speed in self.fan_speed_list:
self._fan_speed = fan_speed
self.schedule_update_ha_state()
def __set_state_to_dock(self):
self._state = STATE_DOCKED
self.schedule_update_ha_state()

View File

@@ -35,6 +35,20 @@ start_pause:
description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
start:
description: Start or resume the cleaning task.
fields:
entity_id:
description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
pause:
description: Pause the cleaning task.
fields:
entity_id:
description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
return_to_base:
description: Tell the vacuum cleaner to return to its dock.
fields:

View File

@@ -9,8 +9,10 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-velbus==2.0.11']
REQUIREMENTS = ['python-velbus==2.0.17']
_LOGGER = logging.getLogger(__name__)
@@ -26,18 +28,76 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
async def async_setup(hass, config):
"""Set up the Velbus platform."""
import velbus
port = config[DOMAIN].get(CONF_PORT)
connection = velbus.VelbusUSBConnection(port)
controller = velbus.Controller(connection)
controller = velbus.Controller(port)
hass.data[DOMAIN] = controller
def stop_velbus(event):
"""Disconnect from serial port."""
_LOGGER.debug("Shutting down ")
connection.stop()
controller.stop()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus)
def callback():
modules = controller.get_modules()
discovery_info = {
'switch': [],
'binary_sensor': []
}
for module in modules:
for channel in range(1, module.number_of_channels() + 1):
for category in discovery_info:
if category in module.get_categories(channel):
discovery_info[category].append((
module.get_module_address(),
channel
))
load_platform(hass, 'switch', DOMAIN,
discovery_info['switch'], config)
load_platform(hass, 'binary_sensor', DOMAIN,
discovery_info['binary_sensor'], config)
controller.scan(callback)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus)
return True
class VelbusEntity(Entity):
"""Representation of a Velbus entity."""
def __init__(self, module, channel):
"""Initialize a Velbus entity."""
self._module = module
self._channel = channel
@property
def unique_id(self):
"""Get unique ID."""
serial = 0
if self._module.serial == 0:
serial = self._module.get_module_address()
else:
serial = self._module.serial
return "{}-{}".format(serial, self._channel)
@property
def name(self):
"""Return the display name of this entity."""
return self._module.get_name(self._channel)
@property
def should_poll(self):
"""Disable polling."""
return False
async def async_added_to_hass(self):
"""Add listener for state changes."""
self._module.on_status_update(self._channel, self._on_update)
def _on_update(self, state):
self.schedule_update_ha_state()

View File

@@ -131,7 +131,7 @@ class OpenWeatherMapWeather(WeatherEntity):
@property
def wind_speed(self):
"""Return the wind speed."""
return self.data.get_wind().get('speed')
return round(self.data.get_wind().get('speed') * 3.6, 2)
@property
def wind_bearing(self):
@@ -173,7 +173,10 @@ class OpenWeatherMapWeather(WeatherEntity):
ATTR_FORECAST_TEMP:
entry.get_temperature('celsius').get('temp'),
ATTR_FORECAST_PRECIPITATION:
entry.get_rain().get('3h'),
(round(entry.get_rain().get('3h'), 1)
if entry.get_rain().get('3h') is not None
and (round(entry.get_rain().get('3h'), 1) > 0)
else None),
ATTR_FORECAST_CONDITION:
[k for k, v in CONDITION_CLASSES.items()
if entry.get_weather_code() in v][0]

View File

@@ -519,8 +519,12 @@ def handle_call_service(hass, connection, msg):
"""
async def call_service_helper(msg):
"""Call a service and fire complete message."""
blocking = True
if (msg['domain'] == 'homeassistant' and
msg['service'] in ['restart', 'stop']):
blocking = False
await hass.services.async_call(
msg['domain'], msg['service'], msg.get('service_data'), True,
msg['domain'], msg['service'], msg.get('service_data'), blocking,
connection.context(msg))
connection.send_message_outside(result_message(msg['id']))

View File

@@ -29,6 +29,10 @@ class ZoneFlowHandler(data_entry_flow.FlowHandler):
"""Initialize zone configuration flow."""
pass
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
errors = {}

View File

@@ -35,7 +35,7 @@ from .discovery_schemas import DISCOVERY_SCHEMAS
from .util import (check_node_schema, check_value_schema, node_name,
check_has_unique_id, is_node_parsed)
REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3']
REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.9']
_LOGGER = logging.getLogger(__name__)

View File

@@ -175,6 +175,7 @@ DISCOVERY_SCHEMAS = [
{const.DISC_COMPONENT: 'lock',
const.DISC_GENERIC_DEVICE_CLASS: [const.GENERIC_TYPE_ENTRY_CONTROL],
const.DISC_SPECIFIC_DEVICE_CLASS: [
const.SPECIFIC_TYPE_DOOR_LOCK,
const.SPECIFIC_TYPE_ADVANCED_DOOR_LOCK,
const.SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK,
const.SPECIFIC_TYPE_SECURE_LOCKBOX],

View File

@@ -24,20 +24,24 @@ Before instantiating the handler, Home Assistant will make sure to load all
dependencies and install the requirements of the component.
At a minimum, each config flow will have to define a version number and the
'init' step.
'user' step.
@config_entries.HANDLERS.register(DOMAIN)
class ExampleConfigFlow(config_entries.FlowHandler):
class ExampleConfigFlow(data_entry_flow.FlowHandler):
VERSION = 1
async def async_step_init(self, user_input=None):
async def async_step_user(self, user_input=None):
The 'init' step is the first step of a flow and is called when a user
The 'user' step is the first step of a flow and is called when a user
starts a new flow. Each step has three different possible results: "Show Form",
"Abort" and "Create Entry".
> Note: prior 0.76, the default step is 'init' step, some config flows still
keep 'init' step to avoid break localization. All new config flow should use
'user' step.
### Show Form
This will show a form to the user to fill in. You define the current step,
@@ -50,7 +54,7 @@ a title, a description and the schema of the data that needs to be returned.
data_schema[vol.Required('password')] = str
return self.async_show_form(
step_id='init',
step_id='user',
title='Account Info',
data_schema=vol.Schema(data_schema)
)
@@ -97,10 +101,10 @@ Assistant, a success message is shown to the user and the flow is finished.
You might want to initialize a config flow programmatically. For example, if
we discover a device on the network that requires user interaction to finish
setup. To do so, pass a source parameter and optional user input to the init
step:
method:
await hass.config_entries.flow.async_init(
'hue', source='discovery', data=discovery_info)
'hue', context={'source': 'discovery'}, data=discovery_info)
The config flow handler will need to add a step to support the source. The step
should follow the same return values as a normal step.
@@ -113,7 +117,7 @@ the flow from the config panel.
import logging
import uuid
from typing import Set, Optional # noqa pylint: disable=unused-import
from typing import Set, Optional, List # noqa pylint: disable=unused-import
from homeassistant import data_entry_flow
from homeassistant.core import callback, HomeAssistant
@@ -123,6 +127,11 @@ from homeassistant.util.decorator import Registry
_LOGGER = logging.getLogger(__name__)
SOURCE_USER = 'user'
SOURCE_DISCOVERY = 'discovery'
SOURCE_IMPORT = 'import'
HANDLERS = Registry()
# Components that have config flows. In future we will auto-generate this list.
FLOWS = [
@@ -151,8 +160,8 @@ ENTRY_STATE_FAILED_UNLOAD = 'failed_unload'
DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery'
DISCOVERY_SOURCES = (
data_entry_flow.SOURCE_DISCOVERY,
data_entry_flow.SOURCE_IMPORT,
SOURCE_DISCOVERY,
SOURCE_IMPORT,
)
EVENT_FLOW_DISCOVERED = 'config_entry_discovered'
@@ -270,19 +279,19 @@ class ConfigEntries:
An instance of this object is available via `hass.config_entries`.
"""
def __init__(self, hass, hass_config):
def __init__(self, hass: HomeAssistant, hass_config: dict) -> None:
"""Initialize the entry manager."""
self.hass = hass
self.flow = data_entry_flow.FlowManager(
hass, self._async_create_flow, self._async_finish_flow)
self._hass_config = hass_config
self._entries = None
self._entries = [] # type: List[ConfigEntry]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
@callback
def async_domains(self):
def async_domains(self) -> List[str]:
"""Return domains for which we have entries."""
seen = set() # type: Set[ConfigEntry]
seen = set() # type: Set[str]
result = []
for entry in self._entries:
@@ -293,7 +302,7 @@ class ConfigEntries:
return result
@callback
def async_entries(self, domain=None):
def async_entries(self, domain: str = None) -> List[ConfigEntry]:
"""Return all entries or entries for a specific domain."""
if domain is None:
return list(self._entries)
@@ -319,7 +328,7 @@ class ConfigEntries:
'require_restart': not unloaded
}
async def async_load(self):
async def async_load(self) -> None:
"""Handle loading the config."""
# Migrating for config entries stored before 0.73
config = await self.hass.helpers.storage.async_migrator(
@@ -374,12 +383,15 @@ class ConfigEntries:
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return None
source = result['source']
if source is None:
source = SOURCE_USER
entry = ConfigEntry(
version=result['version'],
domain=result['handler'],
title=result['title'],
data=result['data'],
source=result['source'],
source=source,
)
self._entries.append(entry)
await self._async_schedule_save()
@@ -399,17 +411,22 @@ class ConfigEntries:
return entry
async def _async_create_flow(self, handler, *, source, data):
async def _async_create_flow(self, handler_key, *, context, data):
"""Create a flow for specified handler.
Handler key is the domain of the component that we want to setup.
"""
component = getattr(self.hass.components, handler)
handler = HANDLERS.get(handler)
component = getattr(self.hass.components, handler_key)
handler = HANDLERS.get(handler_key)
if handler is None:
raise data_entry_flow.UnknownHandler
if context is not None:
source = context.get('source', SOURCE_USER)
else:
source = SOURCE_USER
# Make sure requirements and dependencies of component are resolved
await async_process_deps_reqs(
self.hass, self._hass_config, handler, component)
@@ -424,7 +441,10 @@ class ConfigEntries:
notification_id=DISCOVERY_NOTIFICATION_ID
)
return handler()
flow = handler()
flow.source = source
flow.init_step = source
return flow
async def _async_schedule_save(self):
"""Save the entity registry to a file."""

View File

@@ -1,8 +1,8 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 75
PATCH_VERSION = '1'
MINOR_VERSION = 76
PATCH_VERSION = '0b1'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@@ -423,7 +423,8 @@ class Event:
self.event_type == other.event_type and
self.data == other.data and
self.origin == other.origin and
self.time_fired == other.time_fired)
self.time_fired == other.time_fired and
self.context == other.context)
class EventBus:
@@ -695,7 +696,8 @@ class State:
return (self.__class__ == other.__class__ and # type: ignore
self.entity_id == other.entity_id and
self.state == other.state and
self.attributes == other.attributes)
self.attributes == other.attributes and
self.context == other.context)
def __repr__(self) -> str:
"""Return the representation of the states."""

View File

@@ -2,16 +2,12 @@
import logging
import uuid
import voluptuous as vol
from typing import Dict, Any, Callable, List, Optional # noqa pylint: disable=unused-import
from typing import Dict, Any, Callable, Hashable, List, Optional # noqa pylint: disable=unused-import
from .core import callback, HomeAssistant
from .exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
SOURCE_USER = 'user'
SOURCE_DISCOVERY = 'discovery'
SOURCE_IMPORT = 'import'
RESULT_TYPE_FORM = 'form'
RESULT_TYPE_CREATE_ENTRY = 'create_entry'
RESULT_TYPE_ABORT = 'abort'
@@ -53,22 +49,17 @@ class FlowManager:
'source': flow.source,
} for flow in self._progress.values()]
async def async_init(self, handler: Callable, *, source: str = SOURCE_USER,
data: str = None) -> Any:
async def async_init(self, handler: Hashable, *, context: Dict = None,
data: Any = None) -> Any:
"""Start a configuration flow."""
flow = await self._async_create_flow(handler, source=source, data=data)
flow = await self._async_create_flow(
handler, context=context, data=data)
flow.hass = self.hass
flow.handler = handler
flow.flow_id = uuid.uuid4().hex
flow.source = source
self._progress[flow.flow_id] = flow
if source == SOURCE_USER:
step = 'init'
else:
step = source
return await self._async_handle_step(flow, step, data)
return await self._async_handle_step(flow, flow.init_step, data)
async def async_configure(
self, flow_id: str, user_input: str = None) -> Any:
@@ -131,9 +122,12 @@ class FlowHandler:
flow_id = None
hass = None
handler = None
source = SOURCE_USER
source = None
cur_step = None
# Set by _async_create_flow callback
init_step = 'init'
# Set by developer
VERSION = 1

View File

@@ -22,7 +22,7 @@ class DiscoveryFlowHandler(data_entry_flow.FlowHandler):
self._title = title
self._discovery_function = discovery_function
async def async_step_init(self, user_input=None):
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(

View File

@@ -2,7 +2,7 @@
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant import data_entry_flow, config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
@@ -53,7 +53,8 @@ class FlowManagerIndexView(_BaseFlowManagerView):
handler = data['handler']
try:
result = await self._flow_mgr.async_init(handler)
result = await self._flow_mgr.async_init(
handler, context={'source': config_entries.SOURCE_USER})
except data_entry_flow.UnknownHandler:
return self.json_message('Invalid handler specified', 404)
except data_entry_flow.UnknownStep:

View File

@@ -2,6 +2,7 @@
from collections import OrderedDict
import fnmatch
import re
from typing import Dict
from homeassistant.core import split_entity_id
@@ -9,7 +10,8 @@ from homeassistant.core import split_entity_id
class EntityValues:
"""Class to store entity id based values."""
def __init__(self, exact=None, domain=None, glob=None):
def __init__(self, exact: Dict = None, domain: Dict = None,
glob: Dict = None) -> None:
"""Initialize an EntityConfigDict."""
self._cache = {}
self._exact = exact

View File

@@ -3,7 +3,7 @@ import logging
import signal
import sys
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.const import RESTART_EXIT_CODE
from homeassistant.loader import bind_hass
@@ -12,13 +12,13 @@ _LOGGER = logging.getLogger(__name__)
@callback
@bind_hass
def async_register_signal_handling(hass):
def async_register_signal_handling(hass: HomeAssistant) -> None:
"""Register system signal handler for core."""
if sys.platform != 'win32':
@callback
def async_signal_handle(exit_code):
"""Wrap signal handling."""
hass.async_add_job(hass.async_stop(exit_code))
hass.async_create_task(hass.async_stop(exit_code))
try:
hass.loop.add_signal_handler(

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