Compare commits

..

120 Commits

Author SHA1 Message Date
Paulus Schoutsen 65970a2248 Version bump to 0.72.0b1 2018-06-16 17:36:35 -04:00
Paulus Schoutsen 5d82f48c02 Add experimental UI backend (#15002)
* Add experimental UI

* Add test

* Lint
2018-06-16 17:36:10 -04:00
Teemu R 8e185bc300 Bump pyhs100 version (#15001)
Fixes #13925
2018-06-16 17:36:10 -04:00
Andrey a013908115 Switch to own packaged version of spotipy (#14997) 2018-06-16 17:36:09 -04:00
Sebastian Muszynski bdf6257640 Remove load power attribute for channel USB (#14996)
* Remove load power attribute for channel USB

* Fix format
2018-06-16 17:36:09 -04:00
Paulus Schoutsen 1f50e335fa Bump frontend to 20180616.0 2018-06-16 17:35:52 -04:00
Paulus Schoutsen 87f9f17335 Version bump to 0.72.0b0 2018-06-16 10:51:07 -04:00
Paulus Schoutsen f101f6b7cb Merge remote-tracking branch 'origin/master' into dev 2018-06-16 10:50:43 -04:00
Jason Hu abf07b60f0 Refactoring camera component to use async/await syntax. (#14990)
* Refactoring camera component to use async/await syntax

Also updated camera demo platform to encourage use of async

* Code review
2018-06-16 10:49:11 -04:00
Paulus Schoutsen 0b114f0755 Do not mount deps folder when running in virtual env (#14993)
* Do not mount deps folder when inside virtual env

* Add tests

* Fix package test
2018-06-16 10:48:41 -04:00
Marcelo Moreira de Mello 3ee8f58fdf Upgraded RainCloudy to version 0.0.5 (#14986) 2018-06-16 06:58:18 -04:00
Marcelo Moreira de Mello ff4da05267 Upgraded python-amcrest to 1.2.3 (#14988) 2018-06-16 06:57:58 -04:00
Marcelo Moreira de Mello 17308a2730 Upgraded PyArlo to 0.1.7 (#14987) 2018-06-16 06:57:27 -04:00
cdce8p 7d9bce2153 Fix extended package support (#14980)
* Fix package recurive merge bug

* Fixed extended package support
2018-06-16 06:55:32 -04:00
Marcelo Moreira de Mello 2839f0ff5f Upgrade ring_doorbell to 0.2.1 to fix oauth issues (#14984)
* Upgraded to ring_doorbell to 0.2.1 to fix oauth issues

* Updated unittest to cover Ring oauth
2018-06-16 08:58:39 +02:00
Johan Bloemberg 2ec295a6f8 Add availability to Rflink entities. (#14977) 2018-06-16 00:26:48 +02:00
Ville Skyttä 4bd7a7eee3 Remove inline pylint disables for messages disabled in pylintrc (#14978) 2018-06-16 00:15:46 +02:00
c727 d0cbbe6141 Return ISO formated datetime in forecast (#14975)
* Return ISO formated datetime in forecast

* Lint
2018-06-15 17:09:01 -04:00
John Mihalic 9efa31ef9f Eight Sleep add REM type, Update async syntax, Catch API quirks (#14937) 2018-06-15 15:24:09 -04:00
Paulus Schoutsen 8a777f6e78 Show notification when user configures Nest client_id/secret (#14970)
* Show notification when user configures Nest client_id/secret

* Lint
2018-06-15 15:19:58 -04:00
Robert Svensson ac13a2736b Deconz make groups configurable (#14704)
* Make groups configurable

* Config flow and tests in place

* Fix too long line
2018-06-15 14:31:22 -04:00
Albert Lee 940577e105 Fix binary_sensor.skybell state update when there are no events (#14927) 2018-06-15 14:30:35 -04:00
Sriram Vaidyanathan c917470836 Xiaomi Cameras - multiple models (#14244)
* Added support for Xiaofang Camera

* Added entry for Xiaofang 1080p camera

* Code fix

* Minor comment fix

* Updated coveragerc for Xiaomi cameras

* Added Xiaomi Camera

Added Xiaomi Camera to accommodate multiple models like Yi, Xiaofang, etc.

* Minor code fix

* Minor code fix

* Added model property

* Update xiaomi.py

* Minor code fix

* Update xiaomi.py

* Update xiaomi.py

* Minor code fix

* Package requirement fix due to Version conflict

* To fix conflicts

* Update package_constraints.txt

* Minor fix

* Update xiaomi.py

* Update xiaomi.py

Changes made per comment

* Update xiaomi.py

* Don't update on add.
2018-06-15 14:27:52 -04:00
Paulus Schoutsen 47a344f3a1 Bump frontend to 20180615.0 2018-06-15 13:46:31 -04:00
Paulus Schoutsen f744a29d9d Add calendar panel, add tests (#14973) 2018-06-15 13:37:46 -04:00
Thibault Cohen 3cd4cb741c Add Calendar API endpoint to get events (#14702)
* Add Calendar API endpoint to get events

* Set default event color

* Fix PR comments

* Fix PR comments

* Fix PR comments

* Remote local.py file

* Use iso 8601

* Fix lint

* Fix PR comments

* Fix PR comments

* Add Support for todoist and demo calendar

* Todoist events are allday events

* Add calendar demo api endpoint test

* Register only one api endpoint for calendar

* Rename demo calendar
2018-06-15 11:16:31 -04:00
Paulus Schoutsen 1128104281 Adhere to scan_interval in platforms when setup via config entry (#14969) 2018-06-15 16:59:13 +02:00
Aaron Bach d6d685a483 Fix smappee component - "Error on device update" (#14883) 2018-06-14 19:35:53 -06:00
Paulus Schoutsen 2c6e6c2a6f Add config entry for Sonos + Cast (#14955)
* Add config entry for Sonos

* Lint

* Use add_job

* Add Cast config entry

* Lint

* Rename DOMAIN import

* Mock pychromecast in test
2018-06-14 15:17:54 -04:00
Benedict Aas c8e0de19b6 add relative time option to simulated sensors (#14038)
By default simulated sensors are relative to when they're activated,
instead we make this togglable with this new option 'relative_to_epoch',
and instead they become relative to 1970-01-01 00:00:00.
2018-06-14 14:06:49 -04:00
Paulus Schoutsen b2440a6d95 Fix tests (#14959)
* Fix tests

* Lint
2018-06-14 11:57:09 -04:00
ruohan.chen c36c3f0d64 Add support for ZhongHong HVAC Controllers (#14552)
* first blood for ZhongHong HVAC Controller

* add requirements

* requirements_all.txt updated

* add zhong_hong.py to coveragerc

* add comments

* unique_id add platform name

* zhong_hong_hvac version bump to 1.0.1

* improve some coding style to match the project standard

* zhong_hong_hvac version bump to 1.0.4

* zhong_hong_hvac version require 1.0.7

* update requirements by script/gen_requirements_all.py

* zhong_hong_hvac version bump to 1.0.8

* fix startup problem

* remove unused import

* zhong_hong_hvac version bump to 1.0.9

- operation_mode: cold -> cool

* start hub listen event when all climate entities is ready

* use dispatcher to setup hub

* var name change

SIGNAL_DEVICE_SETTED_UP -> SIGNAL_DEVICE_ADDED

* async problem fix

* bugfix: set_operation_mode forget to use upper case

* stringify the exception instead of print full stack of traceback

* avoid to call str(exception) explicity

* remove unnecessary try...except clause

* remove unused import
2018-06-14 09:47:17 -04:00
Aaron Bach 0e7d284c83 Make AirVisual platform async + other adjustments (#14943)
* Changes complete

* Updated requirements

* Add support for scan_interval

* Small style update

* Owner-requested changes
2018-06-14 09:30:47 -04:00
Nick Whyte cdd111df49 Add sensor.nsw_fuel_station component (#14757)
* Add sensor.nsw_fuel_station component

* bump dependency

* PR Changes

* flake8

* Use MockPrice

* Fix requirements

* Fix tests

* line length

* wip

* Handle errors and show persistent notification

* update tests

* Address @MartinHjelmare's comments

* Fetch station name from API

* Update tests

* Update requirements

* Address comments
2018-06-14 13:56:04 +02:00
Robin cccd0deb65 Fix Facebox face data parsing (#14951)
* Adds parse_faces

* Update facebox.py
2018-06-13 21:02:46 +02:00
Paulus Schoutsen e014a84215 Nest config flow (#14921)
* Move nest to dir based component

* Add config flow for Nest

* Load Nest platforms via config entry

* Add tests for Nest config flow

* Import existing access tokens as config entries

* Lint

* Update coverage

* Update translation

* Fix tests

* Address strings

* Use python-nest token resolution

* Lint

* Do not do I/O inside constructor

* Lint

* Update test requirements
2018-06-13 11:14:52 -04:00
Aaron Bach d549e26a9b Make Yi platform async (#14944)
* Conversion complete

* Updated requirements

* Got rid of 3.6-specific syntax

* Removed more 3.6-specific syntax

* Contributor-requested changes
2018-06-13 11:00:33 -04:00
Marius 08adfd87f7 Add unique_id for mqtt binary sensor (#14929)
* Added unique_id for mqtt binary sensor

* Added missing mqtt message fire in test
2018-06-13 16:20:38 +02:00
Vignesh Venkat 65b0ec6615 Update python-wink to 1.8.0 (#14894)
* wink: Update to python-wink 1.8.0

This pulls in a patch to expose the GE Z-Wave in wall fan switch
as a fan component instead of a light dimmer switch component.

* Update requirements_all.txt
2018-06-13 07:09:42 -04:00
Ville Skyttä cb646e48d0 Upgrade pylint to 1.9.2 (#14916) 2018-06-13 07:08:39 -04:00
ArrayLabs fecce206a9 Myq update from 0.0.8 to 0.0.11 (#14947)
* Update requirements_all.txt

Update myq from 0.0.8 to 0.0.11

* Update myq.py

Update myq from 0.0.8 to 0.0.11
2018-06-13 08:02:27 +02:00
Aaron Bach 176ef411de Add scan_interval to RainMachine (#14945) 2018-06-13 07:30:06 +02:00
Pawel 2ac23c8be6 Epson projector support (#14841)
* Epson projector support. Version based on external library

* Epson projector support. Version based on external library

* modified epson according to MartinHjelmare review.
Added description of cmode to services.yaml

* renamed EPSON_SCHEMA to epson_schema

* removed method of getting cmode property

* removed unnecessary checks
change name of cmode service

* renamed SERVICE_ATTR_CMODE to SERVICE_SELECT_CMODE
2018-06-13 07:28:59 +02:00
Hate-Usernames a373793029 pytradfri 5.5.1: Improved 3rd party bulb support (#14887)
* Bump pytradfri version

* Update light component

* Add tests

* lint

* Docstring typos

* Blank line

* lint

* 5.5.1

* Fix tests on py3.5
2018-06-13 07:17:52 +02:00
Paulus Schoutsen 3153b0c8fc Bump frontend to 20180613.0 2018-06-12 21:20:23 -04:00
Ing. Jaroslav Šafka 89d008d1f3 Fix snapcast uuid to be more unique (#14925)
Current uuid is ok when using only 1 snapserver
New uuid is needed when using multiple snapserver

Because the client can connect to more snapservers and
then uuid based on client MAC is not enough
2018-06-12 15:46:53 +02:00
Christoph Gerneth 6755ae2605 Add support for KIWI Door Locks (#14485)
* initial commit for kiwi door locks

bugfixes

improved attribute display

flake8

more style adjustments

* added session handling

flake8

* added requirements_all

reordered imports and flake8

attempt to pelase a very picky linter

also pleasing pylint now :)

* re-try the build

* added kiwi.py to .coveragerc

* reorganized datetime handling and attribute naming

* created pypi package for door lock library

* updated requirements_all.txt

* code review changes

* added async lock state reset for locking state

* refactored lat/lon attribute updates

* initial locked state changed from undefined to locked

* refactored is_locked property check

* handling authentication exception in setup_platform

* added more check in setup_platform

* code review changes: return type in setup_platform

* fixed logging issue

* event handling in main thread

* updated kiwiki-client to version 0.1.1

* renamed alias e to exc
2018-06-12 12:36:02 +02:00
Jason Hu c18033ba85 Use cv.time_period instead of cv.time_period_str (#14938) 2018-06-12 09:32:13 +02:00
Marcelo Moreira de Mello cdc5388dc9 Refactored Arlo component and enhanced Arlo API queries and times (#14823)
* start arlo refactoring

* Refactored Arlo Hub to avoid uncessary and duplicated GETs to Arlo API

* Refactored Arlo camera component to avoid duplicate queries

* Added debug and error messages when video is not found

* Transformed Arlo Control Panel to Sync

* Makes linter happy

* Uses total_seconds() for scan_interval

* Added callback and fixed scan_interval issue

* Disable multiple tries and supported custom modes set in Arlo

* Bump PyArlo version to 0.1.4

* Makes lint happy

* Removed ArloHub object and added some tweaks

* Fixed hub_refresh method

* Makes lint happy

* Ajusted async syntax and added callbacks decorators

* Bump PyArlo version to 0.1.6 to include some enhacements

* Refined code
2018-06-12 08:01:26 +02:00
Ong Vairoj be4776d039 Add more test cases for samsungtv (#14900)
More test cases to cover retry logic added in 58a1c3839
2018-06-12 07:33:21 +02:00
Jason Hu 30111ea417 Upgrade python-nest, add security_state sensor, nest.set_mode service set ETA as well (#14901) 2018-06-12 07:28:16 +02:00
Erik Eriksson 576c806e86 Update mqtt_eventstream.py (#14923)
* Update mqtt_eventstream.py

Remove a line setting an internal state mqtt_eventstream.initialized to True since:
1. No other platform is doing this
2. This will create an annoying entity/item in the user interface which the user will have to explicitly hide

* Update mqtt_eventstream.py
2018-06-11 15:29:04 +02:00
Matt Snyder 1c561eaf0d Add support for multiple Doorbird stations (#13994) 2018-06-10 19:02:44 +02:00
Ben Lebherz 1da30032a0 Add support for the Unitymedia Horizon HD Recorder (#14275)
* added new platform for the Unitymedia Horizon HD Recorder

* improve connection handling of the horizon platform

* remove unneeded parameters and fix spelling in the horizon platform

* abort or raise exception if connection to the device could not be established

* remove channel/source list and SELECT_SOURCE feature

* remove useless type check after cast and use a try block instead

* abort or raise exception if reconnect to device fails

* remove protocol specific code and restructure sending logic accordingly

* fix indentation to be pep8 complaint

* remove unused methods/properties

* fix unnecessary pylint commands and use a return to abert outside of setup_platform

* directly access config values
2018-06-10 15:38:55 +02:00
Nate Clark d5bbb6ffd2 Add api_host option to Konnected config (#14896) 2018-06-10 13:50:25 +02:00
Dan Klaffenbach b4e5695bbd Bump to denonavr 0.7.3 (#14907)
Closes #14792

See #14794
2018-06-10 13:00:14 +02:00
Yevgeniy 716ab0433f Added daily and hourly modes to Openweathermap (#14875)
* Added daily and hourly modes

Added wind speed and bearing to forecast

* Fix mixed spaces and tabs

* Fix lint

* Fix pylint

* Revert one attribution, order alphabetically
2018-06-10 12:35:10 +02:00
Fabian Affolter 7d9ef97bda Upgrade pylast to 2.3.0 (#14888) 2018-06-10 11:38:35 +02:00
Fabian Affolter 703b4354e0 Upgrade python-mystrom to 0.4.4 (#14889) 2018-06-10 11:38:23 +02:00
Fabian Affolter ce0ca7ff90 Upgrade sendgrid to 5.4.0 (#14891) 2018-06-10 11:38:11 +02:00
Fabian Affolter 54e87836f6 Upgrade psutil to 5.4.6 (#14892) 2018-06-10 11:37:58 +02:00
Fabian Affolter 5f4aa6d2ba Upgrade python_opendata_transport to 0.1.3 (#14905) 2018-06-10 11:37:44 +02:00
Fabian Affolter dc447a75c6 Upgrade pyuptimerobot to 0.0.5 2018-06-10 10:58:45 +02:00
Joakim Sørensen ce7e9e36dd Add Uptime Robot sensor (#14631)
* Added Uptime Robot sensor

* added newline at the end and corrected doclink

* Added changes form @cdce8p

* Convert to binary_sensor

* updated requirements

* moved to correct dir

* Update uptimerobot.py
2018-06-10 10:28:53 +02:00
Aaron Bach 8aca2e84dc Make RainMachine async (#14879)
* Make RainMachine async

* Updated requirements

* Dispatcher adjustments

* Small verbiage change

* Member-requested changes

* Style consistency

* Updated requirements
2018-06-10 10:23:07 +02:00
Fabian Affolter f3e55ce330 Allow different identifiers for the CPU temperature (fixes #10104) (#14898) 2018-06-10 08:39:48 +02:00
Ing. Jaroslav Šafka 20caeb5383 Add entity registry support to media_player.snapcast (#14895)
Unique id for client is generated from prefix 'snapcast_client_'
and MAC address

Unique id for group is generated from prefix 'snapcast_group_'
and UUID provided by snapcast library
2018-06-10 08:31:42 +02:00
hanzoh bc0d0751b9 Add missing mapping of RotaryHandleSensorIP states (#14885) 2018-06-09 16:12:42 +02:00
John Arild Berentsen 5393b073fe Discover Qubino ZMHTDx smart meter switches (#14884) 2018-06-09 15:34:36 +02:00
Ong Vairoj d7b7370c82 Samsung TV can't turn off after idle period (#14587)
When Samsung TV is idle for a period of time after issued a command,
subsequent 'turn_off' command won't turn off the TV. The issue is seen
in Samsung models with websocket as discussed in #12302.

== Reproducible Steps
1. Turn on TV (either via HA or directly).
2. Issue some commands e.g. volume ups / downs.
3. Wait for ~1 minute.
4. Issue turn_off command via HA. TV won't turn off.
5. Issue subsequent turn off commands won't turn off TV still.
6. However, issue some other commands e.g. volume ups / downs multiple
times in a row and then turn_off will turn off the TV.

== Root Cause
The underlying websocket connection opened by samsungctl get closed
after some idle time. There was no retry mechanism so issued commands
would intermittently fail but the subsequent one would succeed when
`_remote` get recreated. With `turn_off()`, however, there is an
additional call to `self.get_remote().close()` which indirectly caused
new connection to be created and then closed immediately. This causes the
component to stuck in failure mode when turn_off command is repeatly
issued.

== The Fix
Recreate the connection and retry the command if connection is closed
to avoid silent failures due to connection closed. Also set `_remote`
to None after calling close() to put it in correct state.

This fix eliminates intermittent command failure and failure mode in
turn_off().
2018-06-09 09:22:34 -04:00
vandenberghev 5f65f67f1e Removed semicolon 2018-06-09 12:37:06 +02:00
Malte Franken f242418986 UVC camera platform handling unavailable NVR or cameras better (#14864)
* fixed tests: using correct camera configuration now and error handling tests must be separated out to ensure that the setup_component call is actually executed

* better error handling during setup; raising PlatformNotReady in likely recoverable cases; added tests
2018-06-09 07:22:17 +02:00
Jason Hu d3d9d9ebf2 Add color_status sensor for Nest Protect (#14868) 2018-06-09 07:16:11 +02:00
Paulus Schoutsen 8ceb57752b Merge pull request #14876 from home-assistant/rc
0.71.0
2018-06-08 18:07:39 -04:00
Paulus Schoutsen bd1af8c3d8 Version bump to 0.71.0 2018-06-08 16:57:59 -04:00
Paulus Schoutsen 6af995026b Add support for new hass.io panel (#14873) 2018-06-08 16:50:19 -04:00
Anders Melchiorsen b657cff6ba Add netgear_lte component (#14687)
* Add netgear_lte component

* Improvements after review

* Allow multiple notify targets

* Require default notify target
2018-06-08 07:46:34 +02:00
Mal Curtis e3fba79126 Disable volume control for Onkyo when unavailable (Closes: #14774) (#14863) 2018-06-08 07:45:21 +02:00
Diogo Gomes bb0068908d Fix unit conversion (#14730)
* reviewing this code min_temp and max_temp are always present and always in celsius

* revert change (preserve unit conversion)

* revert (due to unit conversion)

* self

* clean

* cleaner
2018-06-07 22:44:07 -04:00
Robert Svensson 0748466ffc Zone - Hass configuration name is optional (#14449)
* Hass configuration name is optional

* Check explicitly if name is none

* Reverted back to old logic for zones configured in configuration.yaml, where many zones can have the same name

* New test to verify use case of allowing multiple zones having the same name

* Fix too long line
2018-06-07 17:06:13 -04:00
Dale Higgs fe018fd58c Add set_default_level to logger (#14703)
* Add set_default_service to logger

* Fix 2-line lint error

* Add set_default_level to services.yaml

* Add tests for set_default_level

* Remove function and add else when setting default
2018-06-07 17:03:04 -04:00
Steve Edson 10317a0f71 Rename Hive hub friendly name (#14747)
Right now, "Hub Status" is very generic, I didn't know what component it was coming from, and the only way to tell was searching the source code to find the reference.
2018-06-07 16:57:07 -04:00
Alexei Chetroi 87d55834be zha: handle "step_with_on_off" cluster command in LevelListener. (#14756) 2018-06-07 16:56:07 -04:00
Sebastian Muszynski d4cc806cd5 Fix door/window sensor support of the Xiaomi Aqara LAN protocol V2 (Closes: #14775) (#14777) 2018-06-07 16:55:18 -04:00
Sergiy Maysak 1a7e8c88a3 Wireless tags platform (#13495)
* Initial version of wirelesstags platform support.

* Pinned wirelesstagpy, generated requirements_all.

* Fixed temperature units in imperial units systems, make binary events more tags specific.

* Lowercased tag name during entity_id generation.

* Fixed: tag_id template for tag_binary_events, support of light sensor for homebridge.

* Minor style cleanup.

* Removed state, define_name, icon. Reworked async arm/disarm update. Removed static attrs. Introduced available property. Custom events contains components name now. Cleaned dedundant items from schema definition.

* Removed comment and beep duration from attributes. Minor cleanup of documentation comment.

* Ignoring Wemo switches linked in iOS app.

* Reworked passing data from platform to components using signals.
2018-06-07 16:30:20 -04:00
Paulus Schoutsen 90a51160c4 Don't run unnecessary methods in executor pool (#14853)
* Don't run unnecessary methods in executor pool

* Lint

* Lint 2
2018-06-07 15:31:21 -04:00
Fabian Affolter 50321a29b5 Catch ConnectionError (fixes #14241) (#14748) 2018-06-07 20:25:26 +02:00
Paulus Schoutsen 67d137cfd5 Store config entry id in entity registry (#14851)
* Store config entry id in entity registry

* Lint
2018-06-07 20:23:09 +02:00
Philip Rosenberg-Watt bb4d1773d3 Add min_temp and max_temp to MQTT climate device (#14690)
* Add min_temp and max_temp to MQTT climate device

* Add unit tests

* Remove blank line

* Fix unit tests & temp return values

* PEP-8 fixes

* Remove unused import
2018-06-07 13:50:12 -04:00
Fabian Affolter a6c1192bfc Upgrade aiohttp to 3.3.0 (#14766) 2018-06-07 13:49:14 -04:00
Matthew Treinish d14d2fe588 Add IBM Watson IoT Platform component (#13664)
This commit adds a new history component for the IBM Watson IoT
Platform. The IBM Watson IoT Platform allows for tracking of devices
and analytics on top of the device data. This new component allows
users to have home assistant automatically populate a watson iot
platform board with device data from devices managed by home assistant.
2018-06-07 13:43:51 -04:00
starkillerOG f696331563 Add general sound mode support (#14729)
* Media player: general sound mode support

* General sound mode support

* White spaces

* Add sound mode support to demo media player

* white space

* remove unnessesary code
2018-06-07 10:57:45 -04:00
Anders Melchiorsen 6b2b92a732 Improvements to LIFX reliability (#14848)
* Improve fault tolerance of LIFX initialization

* Update aiolifx to 0.6.3

* Use list comprehension
2018-06-07 10:06:29 -04:00
Fabian Affolter 83ce9450f7 Upgrade Mastodon.py to 1.3.0 (#14858) 2018-06-07 10:04:58 -04:00
Paulus Schoutsen 0b405c33c4 Update Hue flow title (#14852) 2018-06-07 16:00:42 +02:00
Paulus Schoutsen bf74cab7af Fix non awaited test (#14854) 2018-06-07 15:58:54 +02:00
Paulus Schoutsen d8adb4bdb0 Bump frontend to 20180607.0 2018-06-06 22:42:01 -04:00
Sebastian Muszynski bef15264b7 Ignore the mistaken long_both_click event of the 86sw (Closes: #14802) (#14808) 2018-06-06 19:51:59 +02:00
Roman 6d26915c69 Feature/gearbest library update (Closes: #14813) (#14833) 2018-06-06 11:38:50 +02:00
Paulus Schoutsen fa2e6ada26 Route themes and translations over websocket (#14828) 2018-06-06 10:12:43 +02:00
Paulus Schoutsen a6880c452f Migrate entity registry to using websocket (#14830)
* Migrate to using websocket

* Lint
2018-06-06 10:08:36 +02:00
Diogo Gomes 4bccb0d2a1 Merge pull request #14831 from home-assistant/sim-sensor
Limit sensor.simulated to 3 decimals (fixes #14773)
2018-06-05 21:06:11 +01:00
Luc Touraille 103639455c Add Freebox device tracker (#12727)
* Add a device tracker for Freebox routers

* Automatic setup of Freebox device tracker based on discovery

* Make the Freebox device tracker asynchronous
2018-06-05 20:38:50 +02:00
Thomas Krüger 549abd9c7e Improved Fritz!Box thermostat support (#14789) 2018-06-05 20:06:25 +02:00
Fabian Affolter f1aba5511f Limit to 3 decimals (fixes #14773) 2018-06-05 19:44:41 +02:00
Simon Nørager Sørensen 21d05a8b4d Fixes an issue in Xiaomi TV platform that would some TVs not sleep correctly (#14829) 2018-06-05 19:13:16 +02:00
Mischa Gruber cb6c869c2f Action parameter doesn't longer have to be the first parameter (#14815)
* Action parameter doesn't longer have to be the first parameter

* Minified code upon suggestion
2018-06-05 18:15:34 +02:00
Hugo Dupras 640e499964 netatmo api is now in pip as pyatmo (#14824)
Signed-off-by: Hugo D. (jabesq) <jabesq@gmail.com>
2018-06-05 11:55:53 -04:00
Paulus Schoutsen b3b4f7468d Further cleanup frontend (#14805)
* Remove registering panels

* Remove unused image

* Lint
2018-06-05 10:50:16 -04:00
Paulus Schoutsen ad9621ebe5 Use hass iconset (#14185) 2018-06-05 10:49:54 -04:00
Sebastian Muszynski e370d523ec Bump python-miio version (Closes: #13749) (#14796)
* Bump python-miio version

* Fix Xiaomi Power Strip V1 support (Closes: #13749)
2018-06-04 23:50:18 +02:00
vandenberghev 61a41bb8fc Fix issue #14426: [homeassistant.components.sensor] smappee: Error on device update!
https://github.com/home-assistant/home-assistant/issues/14426
2018-06-04 20:08:17 +02:00
vandenberghev 2da6d3c223 Merge branch 'dev' of https://github.com/home-assistant/home-assistant into dev
# Conflicts:
#	homeassistant/components/sensor/smappee.py
2018-06-04 19:44:04 +02:00
Fabian Affolter 816efa02d1 Use pihole module to get data (#14809) 2018-06-04 18:49:26 +02:00
Fabian Affolter bd1b1a9ff9 Update syntax (#14812) 2018-06-04 14:44:55 +02:00
quthla 1d23f7f900 Allow Kodi live streams to be recognized as paused (#14623) 2018-06-04 12:24:28 +01:00
Sebastian Muszynski 39843a73de Add additional 86sw model identifier of the LAN protocol V2 (#14799) 2018-06-04 07:39:50 +02:00
Diogo Gomes aec425d1f6 Weather Platform - IPMA (#14716)
* initial commit

* lint

* update with pyipma

* Added test

* Added test

* lint

* missing dep

* address comments

* lint

* make sure list is iterable

* don't bother with list

* mock dependency

* no need to add test requirements

* last correction
2018-06-03 23:01:48 +02:00
Paulus Schoutsen 855ed2b4e4 Version bump to 0.72.0.dev0 2018-06-03 16:54:23 -04:00
Vincent Van Den Berghe 29e659cf4c Fixed SI units for current consumption 2018-03-13 22:20:56 +01:00
371 changed files with 7655 additions and 1981 deletions
+23 -4
View File
@@ -61,6 +61,9 @@ omit =
homeassistant/components/coinbase.py
homeassistant/components/sensor/coinbase.py
homeassistant/components/cast/*
homeassistant/components/*/cast.py
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
@@ -97,7 +100,7 @@ omit =
homeassistant/components/*/envisalink.py
homeassistant/components/fritzbox.py
homeassistant/components/*/fritzbox.py
homeassistant/components/switch/fritzbox.py
homeassistant/components/eufy.py
homeassistant/components/*/eufy.py
@@ -195,12 +198,15 @@ omit =
homeassistant/components/neato.py
homeassistant/components/*/neato.py
homeassistant/components/nest.py
homeassistant/components/nest/__init__.py
homeassistant/components/*/nest.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/netgear_lte.py
homeassistant/components/*/netgear_lte.py
homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py
@@ -249,6 +255,9 @@ omit =
homeassistant/components/smappee.py
homeassistant/components/*/smappee.py
homeassistant/components/sonos/__init__.py
homeassistant/components/*/sonos.py
homeassistant/components/tado.py
homeassistant/components/*/tado.py
@@ -311,6 +320,9 @@ omit =
homeassistant/components/wink/*
homeassistant/components/*/wink.py
homeassistant/components/wirelesstag.py
homeassistant/components/*/wirelesstag.py
homeassistant/components/xiaomi_aqara.py
homeassistant/components/*/xiaomi_aqara.py
@@ -348,6 +360,7 @@ omit =
homeassistant/components/binary_sensor/ping.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/binary_sensor/tapsaff.py
homeassistant/components/binary_sensor/uptimerobot.py
homeassistant/components/browser.py
homeassistant/components/calendar/caldav.py
homeassistant/components/calendar/todoist.py
@@ -363,6 +376,7 @@ omit =
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py
homeassistant/components/camera/xeoma.py
homeassistant/components/camera/xiaomi.py
homeassistant/components/camera/yi.py
homeassistant/components/climate/econet.py
homeassistant/components/climate/ephember.py
@@ -378,6 +392,7 @@ omit =
homeassistant/components/climate/sensibo.py
homeassistant/components/climate/touchline.py
homeassistant/components/climate/venstar.py
homeassistant/components/climate/zhong_hong.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/gogogate2.py
homeassistant/components/cover/homematic.py
@@ -397,6 +412,7 @@ omit =
homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/freebox.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/gpslogger.py
@@ -462,6 +478,7 @@ omit =
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py
homeassistant/components/lirc.py
homeassistant/components/lock/kiwi.py
homeassistant/components/lock/lockitron.py
homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py
@@ -472,7 +489,6 @@ omit =
homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/bluesound.py
homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/channels.py
homeassistant/components/media_player/clementine.py
homeassistant/components/media_player/cmus.py
@@ -481,10 +497,12 @@ omit =
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/dunehd.py
homeassistant/components/media_player/emby.py
homeassistant/components/media_player/epson.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/frontier_silicon.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py
homeassistant/components/media_player/horizon.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
@@ -506,7 +524,6 @@ omit =
homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/songpal.py
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/spotify.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/ue_smart_radio.py
@@ -644,6 +661,7 @@ omit =
homeassistant/components/sensor/nederlandse_spoorwegen.py
homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nsw_fuel_station.py
homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/ohmconnect.py
@@ -748,6 +766,7 @@ omit =
homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py
homeassistant/components/watson_iot.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
+1
View File
@@ -70,6 +70,7 @@ homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/sma.py @kellerza
+10 -19
View File
@@ -1,5 +1,4 @@
"""Provide methods to bootstrap a Home Assistant instance."""
import asyncio
import logging
import logging.handlers
import os
@@ -17,7 +16,7 @@ from homeassistant.components import persistent_notification
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component
from homeassistant.util.logging import AsyncHandler
from homeassistant.util.package import async_get_user_site, get_user_site
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.signal import async_register_signal_handling
@@ -53,8 +52,9 @@ def from_config_dict(config: Dict[str, Any],
if config_dir is not None:
config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir
hass.loop.run_until_complete(
async_mount_local_lib_path(config_dir, hass.loop))
if not is_virtual_env():
hass.loop.run_until_complete(
async_mount_local_lib_path(config_dir))
# run task
hass = hass.loop.run_until_complete(
@@ -197,7 +197,9 @@ async def async_from_config_file(config_path: str,
# Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
await async_mount_local_lib_path(config_dir, hass.loop)
if not is_virtual_env():
await async_mount_local_lib_path(config_dir)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
@@ -211,9 +213,8 @@ async def async_from_config_file(config_path: str,
finally:
clear_secret_cache()
hass = await async_from_config_dict(
return await async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip)
return hass
@core.callback
@@ -308,23 +309,13 @@ def async_enable_logging(hass: core.HomeAssistant,
"Unable to setup error log %s (access denied)", err_log_path)
def mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path."""
deps_dir = os.path.join(config_dir, 'deps')
lib_dir = get_user_site(deps_dir)
if lib_dir not in sys.path:
sys.path.insert(0, lib_dir)
return deps_dir
async def async_mount_local_lib_path(config_dir: str,
loop: asyncio.AbstractEventLoop) -> str:
async def async_mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path.
This function is a coroutine.
"""
deps_dir = os.path.join(config_dir, 'deps')
lib_dir = await async_get_user_site(deps_dir, loop=loop)
lib_dir = await async_get_user_site(deps_dir)
if lib_dir not in sys.path:
sys.path.insert(0, lib_dir)
return deps_dir
@@ -4,15 +4,17 @@ Support for Arlo Alarm Control Panels.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.arlo/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA)
from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION)
from homeassistant.components.arlo import (
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
@@ -36,21 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Arlo Alarm Control Panels."""
data = hass.data[DATA_ARLO]
arlo = hass.data[DATA_ARLO]
if not data.base_stations:
if not arlo.base_stations:
return
home_mode_name = config.get(CONF_HOME_MODE_NAME)
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
base_stations = []
for base_station in data.base_stations:
for base_station in arlo.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name))
async_add_devices(base_stations, True)
add_devices(base_stations, True)
class ArloBaseStation(AlarmControlPanel):
@@ -68,6 +69,16 @@ class ArloBaseStation(AlarmControlPanel):
"""Return icon."""
return ICON
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def state(self):
"""Return the state of the device."""
@@ -75,30 +86,22 @@ class ArloBaseStation(AlarmControlPanel):
def update(self):
"""Update the state of the device."""
# PyArlo sometimes returns None for mode. So retry 3 times before
# returning None.
num_retries = 3
i = 0
while i < num_retries:
mode = self._base_station.mode
if mode:
self._state = self._get_state_from_mode(mode)
return
i += 1
self._state = None
_LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name)
mode = self._base_station.mode
if mode:
self._state = self._get_state_from_mode(mode)
else:
self._state = None
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
self._base_station.mode = DISARMED
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
async def async_alarm_arm_away(self, code=None):
"""Send arm away command. Uses custom mode."""
self._base_station.mode = self._away_mode_name
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
async def async_alarm_arm_home(self, code=None):
"""Send arm home command. Uses custom mode."""
self._base_station.mode = self._home_mode_name
@@ -125,4 +128,4 @@ class ArloBaseStation(AlarmControlPanel):
return STATE_ALARM_ARMED_HOME
elif mode == self._away_mode_name:
return STATE_ALARM_ARMED_AWAY
return None
return mode
+1 -1
View File
@@ -18,7 +18,7 @@ from homeassistant.const import (
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.2']
REQUIREMENTS = ['amcrest==1.2.3']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
+36 -2
View File
@@ -5,14 +5,18 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/arlo/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL)
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send
REQUIREMENTS = ['pyarlo==0.1.2']
REQUIREMENTS = ['pyarlo==0.1.7']
_LOGGER = logging.getLogger(__name__)
@@ -25,10 +29,16 @@ DOMAIN = 'arlo'
NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Component Setup'
SCAN_INTERVAL = timedelta(seconds=60)
SIGNAL_UPDATE_ARLO = "arlo_update"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -38,6 +48,7 @@ def setup(hass, config):
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
scan_interval = conf.get(CONF_SCAN_INTERVAL)
try:
from pyarlo import PyArlo
@@ -45,7 +56,17 @@ def setup(hass, config):
arlo = PyArlo(username, password, preload=False)
if not arlo.is_connected:
return False
# assign refresh period to base station thread
arlo_base_station = next((
station for station in arlo.base_stations), None)
if arlo_base_station is None:
return False
arlo_base_station.refresh_rate = scan_interval.total_seconds()
hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
hass.components.persistent_notification.create(
@@ -55,4 +76,17 @@ def setup(hass, config):
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
def hub_refresh(event_time):
"""Call ArloHub to refresh information."""
_LOGGER.info("Updating Arlo Hub component")
hass.data[DATA_ARLO].update(update_cameras=True,
update_base_station=True)
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
# register service
hass.services.register(DOMAIN, 'update', hub_refresh)
# register scan interval for ArloHub
track_time_interval(hass, hub_refresh, scan_interval)
return True
+2 -3
View File
@@ -145,7 +145,7 @@ def request_configuration(hass, config, name, host, serialnumber):
def setup(hass, config):
"""Set up for Axis devices."""
def _shutdown(call): # pylint: disable=unused-argument
def _shutdown(call):
"""Stop the event stream on shutdown."""
for serialnumber, device in AXIS_DEVICES.items():
_LOGGER.info("Stopping event stream for %s.", serialnumber)
@@ -272,8 +272,7 @@ class AxisDeviceEvent(Entity):
def _update_callback(self):
"""Update the sensor's state, if needed."""
self.update()
self.schedule_update_ha_state()
self.schedule_update_ha_state(True)
@property
def name(self):
@@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Command line Binary Sensor."""
name = config.get(CONF_NAME)
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.eight_sleep/
"""
import logging
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.eight_sleep import (
@@ -16,8 +15,8 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['eight_sleep']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the eight sleep binary sensor."""
if discovery_info is None:
return
@@ -63,7 +62,6 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):
"""Return true if the binary sensor is on."""
return self._state
@asyncio.coroutine
def async_update(self):
async def async_update(self):
"""Retrieve latest state."""
self._state = self._usrobj.bed_presence
@@ -23,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the GC100 devices."""
binary_sensors = []
@@ -28,7 +28,6 @@ ISY_DEVICE_TYPES = {
}
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 binary sensor platform."""
@@ -299,7 +298,6 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
# No heartbeat timer is active
pass
# pylint: disable=unused-argument
@callback
def timer_elapsed(now) -> None:
"""Heartbeat missed; set state to indicate dead battery."""
@@ -314,7 +312,6 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
self._heartbeat_timer = async_track_point_in_utc_time(
self.hass, timer_elapsed, point_in_time)
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Ignore node status updates.
@@ -115,7 +115,6 @@ class KNXBinarySensor(BinarySensorDevice):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
# pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
+15 -3
View File
@@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.mqtt/
"""
import asyncio
import logging
from typing import Optional
import voluptuous as vol
@@ -24,7 +25,7 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'MQTT Binary sensor'
CONF_UNIQUE_ID = 'unique_id'
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_FORCE_UPDATE = False
@@ -37,6 +38,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
# Integrations shouldn't never expose unique_id through configuration
# this here is an exception because MQTT is a msg transport, not a protocol
vol.Optional(CONF_UNIQUE_ID): cv.string,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@@ -61,7 +65,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_PAYLOAD_OFF),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
value_template
value_template,
config.get(CONF_UNIQUE_ID),
)])
@@ -70,7 +75,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
def __init__(self, name, state_topic, availability_topic, device_class,
qos, force_update, payload_on, payload_off, payload_available,
payload_not_available, value_template):
payload_not_available, value_template,
unique_id: Optional[str]):
"""Initialize the MQTT binary sensor."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
@@ -83,6 +89,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
self._qos = qos
self._force_update = force_update
self._template = value_template
self._unique_id = unique_id
@asyncio.coroutine
def async_added_to_hass(self):
@@ -134,3 +141,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
def force_update(self):
"""Force update."""
return self._force_update
@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@@ -29,6 +29,7 @@ class MyStromView(HomeAssistantView):
url = '/api/mystrom'
name = 'api:mystrom'
supported_actions = ['single', 'double', 'long', 'touch']
def __init__(self, add_devices):
"""Initialize the myStrom URL endpoint."""
@@ -44,16 +45,18 @@ class MyStromView(HomeAssistantView):
@asyncio.coroutine
def _handle(self, hass, data):
"""Handle requests to the myStrom endpoint."""
button_action = list(data.keys())[0]
button_id = data[button_action]
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
button_action = next((
parameter for parameter in data
if parameter in self.supported_actions), None)
if button_action not in ['single', 'double', 'long', 'touch']:
if button_action is None:
_LOGGER.error(
"Received unidentified message from myStrom button: %s", data)
return ("Received unidentified message: {}".format(data),
HTTP_UNPROCESSABLE_ENTITY)
button_id = data[button_action]
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
if entity_id not in self.buttons:
_LOGGER.info("New myStrom button/action detected: %s/%s",
button_id, button_action)
+41 -28
View File
@@ -8,7 +8,8 @@ from itertools import chain
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.nest import DATA_NEST, NestSensorDevice
from homeassistant.components.nest import (
DATA_NEST, DATA_NEST_CONFIG, CONF_BINARY_SENSORS, NestSensorDevice)
from homeassistant.const import CONF_MONITORED_CONDITIONS
DEPENDENCIES = ['nest']
@@ -56,12 +57,19 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Nest binary sensors."""
if discovery_info is None:
return
"""Set up the Nest binary sensors.
No longer used.
"""
async def async_setup_entry(hass, entry, async_add_devices):
"""Set up a Nest binary sensor based on a config entry."""
nest = hass.data[DATA_NEST]
discovery_info = \
hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {})
# Add all available binary sensors if no Nest binary sensor config is set
if discovery_info == {}:
conditions = _VALID_BINARY_SENSOR_TYPES
@@ -76,32 +84,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"for valid options.")
_LOGGER.error(wstr)
sensors = []
for structure in nest.structures():
sensors += [NestBinarySensor(structure, None, variable)
for variable in conditions
if variable in STRUCTURE_BINARY_TYPES]
device_chain = chain(nest.thermostats(),
nest.smoke_co_alarms(),
nest.cameras())
for structure, device in device_chain:
sensors += [NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in BINARY_TYPES]
sensors += [NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in CLIMATE_BINARY_TYPES
and device.is_thermostat]
if device.is_camera:
def get_binary_sensors():
"""Get the Nest binary sensors."""
sensors = []
for structure in nest.structures():
sensors += [NestBinarySensor(structure, None, variable)
for variable in conditions
if variable in STRUCTURE_BINARY_TYPES]
device_chain = chain(nest.thermostats(),
nest.smoke_co_alarms(),
nest.cameras())
for structure, device in device_chain:
sensors += [NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in CAMERA_BINARY_TYPES]
for activity_zone in device.activity_zones:
sensors += [NestActivityZoneSensor(structure,
device,
activity_zone)]
add_devices(sensors, True)
if variable in BINARY_TYPES]
sensors += [NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in CLIMATE_BINARY_TYPES
and device.is_thermostat]
if device.is_camera:
sensors += [NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in CAMERA_BINARY_TYPES]
for activity_zone in device.activity_zones:
sensors += [NestActivityZoneSensor(structure,
device,
activity_zone)]
return sensors
async_add_devices(await hass.async_add_job(get_binary_sensors), True)
class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
@@ -57,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the access to Netatmo binary sensor."""
netatmo = hass.components.netatmo
@@ -68,12 +67,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
module_name = None
import lnetatmo
import pyatmo
try:
data = CameraData(netatmo.NETATMO_AUTH, home)
if not data.get_camera_names():
return None
except lnetatmo.NoDevice:
except pyatmo.NoDevice:
return None
welcome_sensors = config.get(
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the available OctoPrint binary sensors."""
octoprint_api = hass.data[DOMAIN]["api"]
@@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Pilight Binary Sensor."""
disarm = config.get(CONF_DISARM_AFTER_TRIGGER)
@@ -8,7 +8,7 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rainmachine import (
BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE,
BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE,
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH,
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity)
from homeassistant.const import CONF_MONITORED_CONDITIONS
@@ -20,7 +20,8 @@ DEPENDENCIES = ['rainmachine']
_LOGGER = logging.getLogger(__name__)
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 the RainMachine Switch platform."""
if discovery_info is None:
return
@@ -33,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
binary_sensors.append(
RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
add_devices(binary_sensors, True)
async_add_devices(binary_sensors, True)
class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
@@ -70,16 +71,16 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
@callback
def update_data(self):
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, DATA_UPDATE_TOPIC,
self.update_data)
async_dispatcher_connect(
self.hass, SENSOR_UPDATE_TOPIC, self._update_data)
def update(self):
async def async_update(self):
"""Update the state."""
if self._sensor_type == TYPE_FREEZE:
self._state = self.rainmachine.restrictions['current']['freeze']
@@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the raspihats binary_sensor devices."""
I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER]
@@ -39,7 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Raspberry PI GPIO devices."""
pull_mode = config.get(CONF_PULL_MODE)
@@ -94,4 +94,4 @@ class SkybellBinarySensor(SkybellDevice, BinarySensorDevice):
self._state = bool(event and event.get('id') != self._event.get('id'))
self._event = event
self._event = event or {}
@@ -57,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the trend sensors."""
sensors = []
@@ -0,0 +1,92 @@
"""
A platform that to monitor Uptime Robot monitors.
For more details about this platform, please refer to the documentation at
https://www.home-assistant.io/components/binary_sensor.uptimerobot/
"""
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyuptimerobot==0.0.5']
_LOGGER = logging.getLogger(__name__)
ATTR_TARGET = 'target'
CONF_ATTRIBUTION = "Data provided by Uptime Robot"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Uptime Robot binary_sensors."""
from pyuptimerobot import UptimeRobot
up_robot = UptimeRobot()
api_key = config.get(CONF_API_KEY)
monitors = up_robot.getMonitors(api_key)
devices = []
if not monitors or monitors.get('stat') != 'ok':
_LOGGER.error("Error connecting to Uptime Robot")
return
for monitor in monitors['monitors']:
devices.append(UptimeRobotBinarySensor(
api_key, up_robot, monitor['id'], monitor['friendly_name'],
monitor['url']))
add_devices(devices, True)
class UptimeRobotBinarySensor(BinarySensorDevice):
"""Representation of a Uptime Robot binary sensor."""
def __init__(self, api_key, up_robot, monitor_id, name, target):
"""Initialize Uptime Robot the binary sensor."""
self._api_key = api_key
self._monitor_id = str(monitor_id)
self._name = name
self._target = target
self._up_robot = up_robot
self._state = None
@property
def name(self):
"""Return the name of the binary sensor."""
return self._name
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self._state
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'connectivity'
@property
def device_state_attributes(self):
"""Return the state attributes of the binary sensor."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
ATTR_TARGET: self._target,
}
def update(self):
"""Get the latest state of the binary sensor."""
monitor = self._up_robot.getMonitors(self._api_key, self._monitor_id)
if not monitor or monitor.get('stat') != 'ok':
_LOGGER.warning("Failed to get new state")
return
status = monitor['monitors'][0]['status']
self._state = 1 if status == 2 else 0
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Perform the setup for Vera controller devices."""
add_devices(
VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
for device in hass.data[VERA_DEVICES]['binary_sensor'])
[VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
for device in hass.data[VERA_DEVICES]['binary_sensor']], True)
class VeraBinarySensor(VeraDevice, BinarySensorDevice):
@@ -54,6 +54,7 @@ class VerisureDoorWindowSensor(BinarySensorDevice):
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
self._device_label) is not None
# pylint: disable=no-self-use
def update(self):
"""Update the state of the sensor."""
hub.update_overview()
@@ -13,7 +13,7 @@ DEPENDENCIES = ['wemo']
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument, too-many-function-args
# pylint: disable=too-many-function-args
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Register discovered WeMo binary sensors."""
import pywemo.discovery as discovery
@@ -0,0 +1,214 @@
"""
Binary sensor support for Wireless Sensor Tags.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.wirelesstag/
"""
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.wirelesstag import (
DOMAIN as WIRELESSTAG_DOMAIN,
WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
WIRELESSTAG_TYPE_ALSPRO,
WIRELESSTAG_TYPE_WEMO_DEVICE,
SIGNAL_BINARY_EVENT_UPDATE,
WirelessTagBaseSensor)
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, STATE_ON, STATE_OFF)
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['wirelesstag']
_LOGGER = logging.getLogger(__name__)
# On means in range, Off means out of range
SENSOR_PRESENCE = 'presence'
# On means motion detected, Off means cear
SENSOR_MOTION = 'motion'
# On means open, Off means closed
SENSOR_DOOR = 'door'
# On means temperature become too cold, Off means normal
SENSOR_COLD = 'cold'
# On means hot, Off means normal
SENSOR_HEAT = 'heat'
# On means too dry (humidity), Off means normal
SENSOR_DRY = 'dry'
# On means too wet (humidity), Off means normal
SENSOR_WET = 'wet'
# On means light detected, Off means no light
SENSOR_LIGHT = 'light'
# On means moisture detected (wet), Off means no moisture (dry)
SENSOR_MOISTURE = 'moisture'
# On means tag battery is low, Off means normal
SENSOR_BATTERY = 'low_battery'
# Sensor types: Name, device_class, push notification type representing 'on',
# attr to check
SENSOR_TYPES = {
SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', {
"on": "oor",
"off": "back_in_range"
}, 2],
SENSOR_MOTION: ['Motion', 'motion', 'is_moved', {
"on": "motion_detected",
}, 5],
SENSOR_DOOR: ['Door', 'door', 'is_door_open', {
"on": "door_opened",
"off": "door_closed"
}, 5],
SENSOR_COLD: ['Cold', 'cold', 'is_cold', {
"on": "temp_toolow",
"off": "temp_normal"
}, 4],
SENSOR_HEAT: ['Heat', 'heat', 'is_heat', {
"on": "temp_toohigh",
"off": "temp_normal"
}, 4],
SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', {
"on": "too_dry",
"off": "cap_normal"
}, 2],
SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', {
"on": "too_humid",
"off": "cap_normal"
}, 2],
SENSOR_LIGHT: ['Light', 'light', 'is_light_on', {
"on": "too_bright",
"off": "light_normal"
}, 1],
SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', {
"on": "water_detected",
"off": "water_dried",
}, 1],
SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', {
"on": "low_battery"
}, 3]
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the platform for a WirelessTags."""
platform = hass.data.get(WIRELESSTAG_DOMAIN)
sensors = []
tags = platform.tags
for tag in tags.values():
allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag)
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type in allowed_sensor_types:
sensors.append(WirelessTagBinarySensor(platform, tag,
sensor_type))
add_devices(sensors, True)
hass.add_job(platform.install_push_notifications, sensors)
class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
"""A binary sensor implementation for WirelessTags."""
@classmethod
def allowed_sensors(cls, tag):
"""Return list of allowed sensor types for specific tag type."""
sensors_map = {
# 13-bit tag - allows everything but not light and moisture
WIRELESSTAG_TYPE_13BIT: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_MOTION, SENSOR_DOOR,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_DRY, SENSOR_WET],
# Moister/water sensor - temperature and moisture only
WIRELESSTAG_TYPE_WATER: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_MOISTURE],
# ALS Pro: allows everything, but not moisture
WIRELESSTAG_TYPE_ALSPRO: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_MOTION, SENSOR_DOOR,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_DRY, SENSOR_WET,
SENSOR_LIGHT],
# Wemo are power switches.
WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE]
}
# allow everything if tag type is unknown
# (i just dont have full catalog of them :))
tag_type = tag.tag_type
fullset = SENSOR_TYPES.keys()
return sensors_map[tag_type] if tag_type in sensors_map else fullset
def __init__(self, api, tag, sensor_type):
"""Initialize a binary sensor for a Wireless Sensor Tags."""
super().__init__(api, tag)
self._sensor_type = sensor_type
self._name = '{0} {1}'.format(self._tag.name,
SENSOR_TYPES[self._sensor_type][0])
self._device_class = SENSOR_TYPES[self._sensor_type][1]
self._tag_attr = SENSOR_TYPES[self._sensor_type][2]
self.binary_spec = SENSOR_TYPES[self._sensor_type][3]
self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4]
async def async_added_to_hass(self):
"""Register callbacks."""
tag_id = self.tag_id
event_type = self.device_class
async_dispatcher_connect(
self.hass,
SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type),
self._on_binary_event_callback)
@property
def is_on(self):
"""Return True if the binary sensor is on."""
return self._state == STATE_ON
@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class
@property
def principal_value(self):
"""Return value of tag.
Subclasses need override based on type of sensor.
"""
return (
STATE_ON if getattr(self._tag, self._tag_attr, False)
else STATE_OFF)
def updated_state_value(self):
"""Use raw princial value."""
return self.principal_value
@callback
def _on_binary_event_callback(self, event):
"""Update state from arrive push notification."""
# state should be 'on' or 'off'
self._state = event.data.get('state')
self.async_schedule_update_ha_state()
@@ -28,7 +28,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
devices.append(XiaomiMotionSensor(device, hass, gateway))
elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
devices.append(XiaomiDoorSensor(device, gateway))
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'window_status'
devices.append(XiaomiDoorSensor(device, data_key, gateway))
elif model == 'sensor_wleak.aq1':
devices.append(XiaomiWaterLeakSensor(device, gateway))
elif model in ['smoke', 'sensor_smoke']:
@@ -43,10 +47,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
data_key = 'channel_0'
devices.append(XiaomiButton(device, 'Switch', data_key,
hass, gateway))
elif model in ['86sw1', 'sensor_86sw1.aq1']:
elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
hass, gateway))
elif model in ['86sw2', 'sensor_86sw2.aq1']:
elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
'channel_0', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
@@ -190,11 +194,11 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
class XiaomiDoorSensor(XiaomiBinarySensor):
"""Representation of a XiaomiDoorSensor."""
def __init__(self, device, xiaomi_hub):
def __init__(self, device, data_key, xiaomi_hub):
"""Initialize the XiaomiDoorSensor."""
self._open_since = 0
XiaomiBinarySensor.__init__(self, device, 'Door Window Sensor',
xiaomi_hub, 'status', 'opening')
xiaomi_hub, data_key, 'opening')
@property
def device_state_attributes(self):
@@ -330,7 +334,7 @@ class XiaomiButton(XiaomiBinarySensor):
click_type = 'both'
elif value == 'shake':
click_type = 'shake'
elif value == 'long_click':
elif value in ['long_click', 'long_both_click']:
return False
else:
_LOGGER.warning("Unsupported click_type detected: %s", value)
@@ -187,8 +187,8 @@ class Switch(zha.Entity, BinarySensorDevice):
if args[0] == 0xff:
rate = 10 # Should read default move rate
self._entity.move_level(-rate if args[0] else rate)
elif command_id == 0x0002: # step
# Step (technically shouldn't change on/off)
elif command_id in (0x0002, 0x0006): # step, -with_on_off
# Step (technically may change on/off)
self._entity.move_level(-args[1] if args[0] else args[1])
def attribute_update(self, attrid, value):
-1
View File
@@ -34,7 +34,6 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument
def setup(hass, config):
"""Set up the BloomSky component."""
api_key = config[DOMAIN][CONF_API_KEY]
+76 -16
View File
@@ -4,11 +4,12 @@ Support for Google Calendar event device sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/calendar/
"""
import asyncio
import logging
from datetime import timedelta
import re
from aiohttp import web
from homeassistant.components.google import (
CONF_OFFSET, CONF_DEVICE_ID, CONF_NAME)
from homeassistant.const import STATE_OFF, STATE_ON
@@ -18,23 +19,32 @@ from homeassistant.helpers.entity import Entity, generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.util import dt
from homeassistant.components import http
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'calendar'
DEPENDENCIES = ['http']
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SCAN_INTERVAL = timedelta(seconds=60)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Track states and offer events for calendars."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
yield from component.async_setup(config)
hass.http.register_view(CalendarListView(component))
hass.http.register_view(CalendarEventView(component))
await hass.components.frontend.async_register_built_in_panel(
'calendar', 'calendar', 'hass:calendar')
await component.async_setup(config)
return True
@@ -42,7 +52,14 @@ DEFAULT_CONF_TRACK_NEW = True
DEFAULT_CONF_OFFSET = '!!'
# pylint: disable=too-many-instance-attributes
def get_date(date):
"""Get the dateTime from date or dateTime as a local."""
if 'date' in date:
return dt.start_of_local_day(dt.dt.datetime.combine(
dt.parse_date(date['date']), dt.dt.time.min))
return dt.as_local(dt.parse_datetime(date['dateTime']))
class CalendarEventDevice(Entity):
"""A calendar event device."""
@@ -50,7 +67,6 @@ class CalendarEventDevice(Entity):
# with an update() method
data = None
# pylint: disable=too-many-arguments
def __init__(self, hass, data):
"""Create the Calendar Event Device."""
self._name = data.get(CONF_NAME)
@@ -144,15 +160,8 @@ class CalendarEventDevice(Entity):
self.cleanup()
return
def _get_date(date):
"""Get the dateTime from date or dateTime as a local."""
if 'date' in date:
return dt.start_of_local_day(dt.dt.datetime.combine(
dt.parse_date(date['date']), dt.dt.time.min))
return dt.as_local(dt.parse_datetime(date['dateTime']))
start = _get_date(self.data.event['start'])
end = _get_date(self.data.event['end'])
start = get_date(self.data.event['start'])
end = get_date(self.data.event['end'])
summary = self.data.event.get('summary', '')
@@ -176,10 +185,61 @@ class CalendarEventDevice(Entity):
# cleanup the string so we don't have a bunch of double+ spaces
self._cal_data['message'] = re.sub(' +', '', summary).strip()
self._cal_data['offset_time'] = offset_time
self._cal_data['location'] = self.data.event.get('location', '')
self._cal_data['description'] = self.data.event.get('description', '')
self._cal_data['start'] = start
self._cal_data['end'] = end
self._cal_data['all_day'] = 'date' in self.data.event['start']
class CalendarEventView(http.HomeAssistantView):
"""View to retrieve calendar content."""
url = '/api/calendars/{entity_id}'
name = 'api:calendars:calendar'
def __init__(self, component):
"""Initialize calendar view."""
self.component = component
async def get(self, request, entity_id):
"""Return calendar events."""
entity = self.component.get_entity(entity_id)
start = request.query.get('start')
end = request.query.get('end')
if None in (start, end, entity):
return web.Response(status=400)
try:
start_date = dt.parse_datetime(start)
end_date = dt.parse_datetime(end)
except (ValueError, AttributeError):
return web.Response(status=400)
event_list = await entity.async_get_events(
request.app['hass'], start_date, end_date)
return self.json(event_list)
class CalendarListView(http.HomeAssistantView):
"""View to retrieve calendar list."""
url = '/api/calendars'
name = "api:calendars"
def __init__(self, component):
"""Initialize calendar view."""
self.component = component
async def get(self, request):
"""Retrieve calendar list."""
get_state = request.app['hass'].states.get
calendar_list = []
for entity in self.component.entities:
state = get_state(entity.entity_id)
calendar_list.append({
"name": state.name,
"entity_id": entity.entity_id,
})
return self.json(sorted(calendar_list, key=lambda x: x['name']))
+33 -2
View File
@@ -11,7 +11,7 @@ import re
import voluptuous as vol
from homeassistant.components.calendar import (
PLATFORM_SCHEMA, CalendarEventDevice)
PLATFORM_SCHEMA, CalendarEventDevice, get_date)
from homeassistant.const import (
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
import homeassistant.helpers.config_validation as cv
@@ -92,7 +92,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
if not config.get(CONF_CUSTOM_CALENDARS):
device_data = {
CONF_NAME: calendar.name,
CONF_DEVICE_ID: calendar.name
CONF_DEVICE_ID: calendar.name,
}
calendar_devices.append(
WebDavCalendarEventDevice(hass, device_data, calendar)
@@ -120,6 +120,10 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
attributes = super().device_state_attributes
return attributes
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
return await self.data.async_get_events(hass, start_date, end_date)
class WebDavCalendarData(object):
"""Class to utilize the calendar dav client object to get next event."""
@@ -131,6 +135,33 @@ class WebDavCalendarData(object):
self.search = search
self.event = None
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
# Get event list from the current calendar
vevent_list = await hass.async_add_job(self.calendar.date_search,
start_date, end_date)
event_list = []
for event in vevent_list:
vevent = event.instance.vevent
uid = None
if hasattr(vevent, 'uid'):
uid = vevent.uid.value
data = {
"uid": uid,
"title": vevent.summary.value,
"start": self.get_hass_date(vevent.dtstart.value),
"end": self.get_hass_date(self.get_end_date(vevent)),
"location": self.get_attr_value(vevent, "location"),
"description": self.get_attr_value(vevent, "description"),
}
data['start'] = get_date(data['start']).isoformat()
data['end'] = get_date(data['end']).isoformat()
event_list.append(data)
return event_list
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
+21 -5
View File
@@ -4,8 +4,10 @@ Demo platform that has two fake binary sensors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import copy
import homeassistant.util.dt as dt_util
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.calendar import CalendarEventDevice, get_date
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
@@ -15,13 +17,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
calendar_data_current = DemoGoogleCalendarDataCurrent()
add_devices([
DemoGoogleCalendar(hass, calendar_data_future, {
CONF_NAME: 'Future Event',
CONF_DEVICE_ID: 'future_event',
CONF_NAME: 'Calendar 1',
CONF_DEVICE_ID: 'calendar_1',
}),
DemoGoogleCalendar(hass, calendar_data_current, {
CONF_NAME: 'Current Event',
CONF_DEVICE_ID: 'current_event',
CONF_NAME: 'Calendar 2',
CONF_DEVICE_ID: 'calendar_2',
}),
])
@@ -29,11 +31,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoGoogleCalendarData(object):
"""Representation of a Demo Calendar element."""
event = {}
# pylint: disable=no-self-use
def update(self):
"""Return true so entity knows we have new data."""
return True
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
event = copy.copy(self.event)
event['title'] = event['summary']
event['start'] = get_date(event['start']).isoformat()
event['end'] = get_date(event['end']).isoformat()
return [event]
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a future event."""
@@ -80,3 +92,7 @@ class DemoGoogleCalendar(CalendarEventDevice):
"""Initialize Google Calendar but without the API calls."""
self.data = calendar_data
super().__init__(hass, data)
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
return await self.data.async_get_events(hass, start_date, end_date)
+35 -5
View File
@@ -51,6 +51,10 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
super().__init__(hass, data)
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
return await self.data.async_get_events(hass, start_date, end_date)
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
@@ -64,9 +68,7 @@ class GoogleCalendarData(object):
self.ignore_availability = ignore_availability
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
def _prepare_query(self):
from httplib2 import ServerNotFoundError
try:
@@ -74,13 +76,41 @@ class GoogleCalendarData(object):
except ServerNotFoundError:
_LOGGER.warning("Unable to connect to Google, using cached data")
return False
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id
if self.search:
params['q'] = self.search
return service, params
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
service, params = await hass.async_add_job(self._prepare_query)
params['timeMin'] = start_date.isoformat('T')
params['timeMax'] = end_date.isoformat('T')
# pylint: disable=no-member
events = await hass.async_add_job(service.events)
# pylint: enable=no-member
result = await hass.async_add_job(events.list(**params).execute)
items = result.get('items', [])
event_list = []
for item in items:
if (not self.ignore_availability
and 'transparency' in item.keys()):
if item['transparency'] == 'opaque':
event_list.append(item)
else:
event_list.append(item)
return event_list
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
service, params = self._prepare_query()
params['timeMin'] = dt.now().isoformat('T')
events = service.events() # pylint: disable=no-member
result = events.list(**params).execute()
@@ -257,6 +257,10 @@ class TodoistProjectDevice(CalendarEventDevice):
super().cleanup()
self._cal_data[ALL_TASKS] = []
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
return await self.data.async_get_events(hass, start_date, end_date)
@property
def device_state_attributes(self):
"""Return the device state attributes."""
@@ -485,6 +489,31 @@ class TodoistProjectData(object):
continue
return event
async def async_get_events(self, hass, start_date, end_date):
"""Get all tasks in a specific time frame."""
if self._id is None:
project_task_data = [
task for task in self._api.state[TASKS]
if not self._project_id_whitelist or
task[PROJECT_ID] in self._project_id_whitelist]
else:
project_task_data = self._api.projects.get_data(self._id)[TASKS]
events = []
time_format = '%a %d %b %Y %H:%M:%S %z'
for task in project_task_data:
due_date = datetime.strptime(task['due_date_utc'], time_format)
if due_date > start_date and due_date < end_date:
event = {
'uid': task['id'],
'title': task['content'],
'start': due_date.isoformat(),
'end': due_date.isoformat(),
'allDay': True,
}
events.append(event)
return events
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
+26 -21
View File
@@ -1,4 +1,3 @@
# pylint: disable=too-many-lines
"""
Component to interface with cameras.
@@ -97,6 +96,7 @@ def disable_motion_detection(hass, entity_id=None):
@bind_hass
@callback
def async_snapshot(hass, filename, entity_id=None):
"""Make a snapshot from a camera."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@@ -129,8 +129,7 @@ async def async_get_image(hass, entity_id, timeout=10):
raise HomeAssistantError('Unable to get image')
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the camera component."""
component = hass.data[DOMAIN] = \
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
@@ -142,7 +141,7 @@ def async_setup(hass, config):
SCHEMA_WS_CAMERA_THUMBNAIL
)
yield from component.async_setup(config)
await component.async_setup(config)
@callback
def update_tokens(time):
@@ -154,27 +153,25 @@ def async_setup(hass, config):
hass.helpers.event.async_track_time_interval(
update_tokens, TOKEN_CHANGE_INTERVAL)
@asyncio.coroutine
def async_handle_camera_service(service):
async def async_handle_camera_service(service):
"""Handle calls to the camera services."""
target_cameras = component.async_extract_from_service(service)
update_tasks = []
for camera in target_cameras:
if service.service == SERVICE_ENABLE_MOTION:
yield from camera.async_enable_motion_detection()
await camera.async_enable_motion_detection()
elif service.service == SERVICE_DISABLE_MOTION:
yield from camera.async_disable_motion_detection()
await camera.async_disable_motion_detection()
if not camera.should_poll:
continue
update_tasks.append(camera.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
@asyncio.coroutine
def async_handle_snapshot_service(service):
async def async_handle_snapshot_service(service):
"""Handle snapshot services calls."""
target_cameras = component.async_extract_from_service(service)
filename = service.data[ATTR_FILENAME]
@@ -190,7 +187,7 @@ def async_setup(hass, config):
"Can't write %s, no access to path!", snapshot_file)
continue
image = yield from camera.async_camera_image()
image = await camera.async_camera_image()
def _write_image(to_file, image_data):
"""Executor helper to write image."""
@@ -198,7 +195,7 @@ def async_setup(hass, config):
img_file.write(image_data)
try:
yield from hass.async_add_job(
await hass.async_add_job(
_write_image, snapshot_file, image)
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)
@@ -216,6 +213,16 @@ def async_setup(hass, config):
return True
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class Camera(Entity):
"""The base class for camera entities."""
@@ -265,6 +272,7 @@ class Camera(Entity):
"""Return bytes of camera image."""
raise NotImplementedError()
@callback
def async_camera_image(self):
"""Return bytes of camera image.
@@ -388,8 +396,7 @@ class CameraView(HomeAssistantView):
"""Initialize a basic camera view."""
self.component = component
@asyncio.coroutine
def get(self, request, entity_id):
async def get(self, request, entity_id):
"""Start a GET request."""
camera = self.component.get_entity(entity_id)
@@ -403,11 +410,10 @@ class CameraView(HomeAssistantView):
if not authenticated:
return web.Response(status=401)
response = yield from self.handle(request, camera)
response = await self.handle(request, camera)
return response
@asyncio.coroutine
def handle(self, request, camera):
async def handle(self, request, camera):
"""Handle the camera request."""
raise NotImplementedError()
@@ -418,12 +424,11 @@ class CameraImageView(CameraView):
url = '/api/camera_proxy/{entity_id}'
name = 'api:camera:image'
@asyncio.coroutine
def handle(self, request, camera):
async def handle(self, request, camera):
"""Serve camera image."""
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(10, loop=request.app['hass'].loop):
image = yield from camera.async_camera_image()
image = await camera.async_camera_image()
if image:
return web.Response(body=image,
+26 -29
View File
@@ -4,23 +4,22 @@ Support for Netgear Arlo IP cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.arlo/
"""
import asyncio
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
from homeassistant.components.arlo import (
DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO)
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=90)
ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed'
@@ -44,22 +43,19 @@ POWERSAVE_MODE_MAPPING = {
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS):
cv.string,
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Arlo IP Camera."""
arlo = hass.data.get(DATA_ARLO)
if not arlo:
return False
arlo = hass.data[DATA_ARLO]
cameras = []
for camera in arlo.cameras:
cameras.append(ArloCam(hass, camera, config))
add_devices(cameras, True)
add_devices(cameras)
class ArloCam(Camera):
@@ -74,31 +70,41 @@ class ArloCam(Camera):
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._last_refresh = None
if self._camera.base_station:
self._camera.base_station.refresh_rate = \
SCAN_INTERVAL.total_seconds()
self.attrs = {}
def camera_image(self):
"""Return a still image response from the camera."""
return self._camera.last_image
return self._camera.last_image_from_cache
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state()
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
video = self._camera.last_video
if not video:
error_msg = \
'Video not found for {0}. Is it older than {1} days?'.format(
self.name, self._camera.min_days_vdo_cache)
_LOGGER.error(error_msg)
return
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
yield from stream.open_camera(
await stream.open_camera(
video.video_url, extra_cmd=self._ffmpeg_arguments)
yield from async_aiohttp_proxy_stream(
await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close()
await stream.close()
@property
def name(self):
@@ -132,11 +138,6 @@ class ArloCam(Camera):
"""Return the camera brand."""
return DEFAULT_BRAND
@property
def should_poll(self):
"""Camera should poll periodically."""
return True
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
@@ -164,7 +165,3 @@ class ArloCam(Camera):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False
self.set_base_station_mode(ARLO_MODE_DISARMED)
def update(self):
"""Add an attribute-update task to the executor pool."""
self._camera.update()
@@ -13,7 +13,6 @@ from homeassistant.components.camera import Camera
DEPENDENCIES = ['bloomsky']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to BloomSky cameras."""
bloomsky = hass.components.bloomsky
+3 -2
View File
@@ -12,9 +12,10 @@ from homeassistant.components.camera import Camera
_LOGGER = logging.getLogger(__name__)
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 the Demo camera platform."""
add_devices([
async_add_devices([
DemoCamera(hass, config, 'Demo camera')
])
+19 -13
View File
@@ -17,9 +17,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
DEPENDENCIES = ['doorbird']
_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
_CAMERA_LAST_MOTION = "DoorBird Last Motion"
_CAMERA_LIVE = "DoorBird Live"
_CAMERA_LAST_VISITOR = "{} Last Ring"
_CAMERA_LAST_MOTION = "{} Last Motion"
_CAMERA_LIVE = "{} Live"
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1)
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
@@ -30,16 +30,22 @@ _TIMEOUT = 10 # seconds
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the DoorBird camera platform."""
device = hass.data.get(DOORBIRD_DOMAIN)
async_add_devices([
DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, _LIVE_INTERVAL),
DoorBirdCamera(
device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR,
_LAST_VISITOR_INTERVAL),
DoorBirdCamera(
device.history_image_url(1, 'motionsensor'), _CAMERA_LAST_MOTION,
_LAST_MOTION_INTERVAL),
])
for doorstation in hass.data[DOORBIRD_DOMAIN]:
device = doorstation.device
async_add_devices([
DoorBirdCamera(
device.live_image_url,
_CAMERA_LIVE.format(doorstation.name),
_LIVE_INTERVAL),
DoorBirdCamera(
device.history_image_url(1, 'doorbell'),
_CAMERA_LAST_VISITOR.format(doorstation.name),
_LAST_VISITOR_INTERVAL),
DoorBirdCamera(
device.history_image_url(1, 'motionsensor'),
_CAMERA_LAST_MOTION.format(doorstation.name),
_LAST_MOTION_INTERVAL),
])
class DoorBirdCamera(Camera):
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a Foscam IP Camera."""
add_devices([FoscamCam(config)])
@@ -46,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a generic IP Camera."""
async_add_devices([GenericCamera(hass, config)])
-1
View File
@@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a MJPEG IP Camera."""
if discovery_info:
+10 -5
View File
@@ -23,14 +23,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a Nest Cam."""
if discovery_info is None:
return
"""Set up a Nest Cam.
camera_devices = hass.data[nest.DATA_NEST].cameras()
No longer in use.
"""
async def async_setup_entry(hass, entry, async_add_devices):
"""Set up a Nest sensor based on a config entry."""
camera_devices = \
await hass.async_add_job(hass.data[nest.DATA_NEST].cameras)
cameras = [NestCamera(structure, device)
for structure, device in camera_devices]
add_devices(cameras, True)
async_add_devices(cameras, True)
class NestCamera(Camera):
+2 -3
View File
@@ -29,13 +29,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to Netatmo cameras."""
netatmo = hass.components.netatmo
home = config.get(CONF_HOME)
verify_ssl = config.get(CONF_VERIFY_SSL, True)
import lnetatmo
import pyatmo
try:
data = CameraData(netatmo.NETATMO_AUTH, home)
for camera_name in data.get_camera_names():
@@ -46,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
continue
add_devices([NetatmoCamera(data, camera_name, home,
camera_type, verify_ssl)])
except lnetatmo.NoDevice:
except pyatmo.NoDevice:
return None
+14 -12
View File
@@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant.const import CONF_PORT
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
REQUIREMENTS = ['uvcclient==0.10.1']
@@ -41,25 +42,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config[CONF_PORT]
from uvcclient import nvr
nvrconn = nvr.UVCRemote(addr, port, key)
try:
# Exceptions may be raised in all method calls to the nvr library.
nvrconn = nvr.UVCRemote(addr, port, key)
cameras = nvrconn.index()
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
# Filter out airCam models, which are not supported in the latest
# version of UnifiVideo and which are EOL by Ubiquiti
cameras = [
camera for camera in cameras
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
except nvr.NotAuthorized:
_LOGGER.error("Authorization failure while connecting to NVR")
return False
except nvr.NvrError:
_LOGGER.error("NVR refuses to talk to me")
return False
except nvr.NvrError as ex:
_LOGGER.error("NVR refuses to talk to me: %s", str(ex))
raise PlatformNotReady
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to NVR: %s", str(ex))
return False
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
# Filter out airCam models, which are not supported in the latest
# version of UnifiVideo and which are EOL by Ubiquiti
cameras = [
camera for camera in cameras
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
raise PlatformNotReady
add_devices([UnifiVideoCamera(nvrconn,
camera[identifier],
+166
View File
@@ -0,0 +1,166 @@
"""
This component provides support for Xiaomi Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.xiaomi/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH,
CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
DEFAULT_BRAND = 'Xiaomi Home Camera'
DEFAULT_PATH = '/media/mmcblk0p1/record'
DEFAULT_PORT = 21
DEFAULT_USERNAME = 'root'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
CONF_MODEL = 'model'
MODEL_YI = 'yi'
MODEL_XIAOFANG = 'xiaofang'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_MODEL): vol.Any(MODEL_YI,
MODEL_XIAOFANG),
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string
})
async def async_setup_platform(hass,
config,
async_add_devices,
discovery_info=None):
"""Set up a Xiaomi Camera."""
_LOGGER.debug('Received configuration for model %s', config[CONF_MODEL])
async_add_devices([XiaomiCamera(hass, config)])
class XiaomiCamera(Camera):
"""Define an implementation of a Xiaomi Camera."""
def __init__(self, hass, config):
"""Initialize."""
super().__init__()
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
self._last_image = None
self._last_url = None
self._manager = hass.data[DATA_FFMPEG]
self._name = config[CONF_NAME]
self.host = config[CONF_HOST]
self._model = config[CONF_MODEL]
self.port = config[CONF_PORT]
self.path = config[CONF_PATH]
self.user = config[CONF_USERNAME]
self.passwd = config[CONF_PASSWORD]
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def brand(self):
"""Return the camera brand."""
return DEFAULT_BRAND
@property
def model(self):
"""Return the camera model."""
return self._model
def get_latest_video_url(self):
"""Retrieve the latest video file from the Xiaomi Camera FTP server."""
from ftplib import FTP, error_perm
ftp = FTP(self.host)
try:
ftp.login(self.user, self.passwd)
except error_perm as exc:
_LOGGER.error('Camera login failed: %s', exc)
return False
try:
ftp.cwd(self.path)
except error_perm as exc:
_LOGGER.error('Unable to find path: %s - %s', self.path, exc)
return False
dirs = [d for d in ftp.nlst() if '.' not in d]
if not dirs:
if self._model == MODEL_YI:
_LOGGER.warning("There don't appear to be any uploaded videos")
return False
elif self._model == MODEL_XIAOFANG:
_LOGGER.warning("There don't appear to be any folders")
return False
first_dir = dirs[-1]
try:
ftp.cwd(first_dir)
except error_perm as exc:
_LOGGER.error('Unable to find path: %s - %s', first_dir, exc)
return False
dirs = [d for d in ftp.nlst() if '.' not in d]
if not dirs:
_LOGGER.warning("There don't appear to be any uploaded videos")
return False
latest_dir = dirs[-1]
ftp.cwd(latest_dir)
videos = [v for v in ftp.nlst() if '.tmp' not in v]
if not videos:
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
return False
if self._model == MODEL_XIAOFANG:
video = videos[-2]
else:
video = videos[-1]
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}'.format(
self.user, self.passwd, self.host, self.port, ftp.pwd(), video)
async def async_camera_image(self):
"""Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG
url = await self.hass.async_add_job(self.get_latest_video_url)
if url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
self._last_image = await asyncio.shield(ffmpeg.get_image(
url, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments), loop=self.hass.loop)
self._last_url = url
return self._last_image
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
await stream.open_camera(
self._last_url, extra_cmd=self._extra_arguments)
await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
await stream.close()
+59 -50
View File
@@ -11,11 +11,13 @@ import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH,
CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PATH, CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.exceptions import PlatformNotReady
REQUIREMENTS = ['aioftp==0.10.1']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
@@ -38,12 +40,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
async def async_setup_platform(hass,
config,
async_add_devices,
discovery_info=None):
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up a Yi Camera."""
_LOGGER.debug('Received configuration: %s', config)
async_add_devices([YiCamera(hass, config)], True)
@@ -54,71 +53,81 @@ class YiCamera(Camera):
"""Initialize."""
super().__init__()
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
self._ftp = None
self._last_image = None
self._last_url = None
self._manager = hass.data[DATA_FFMPEG]
self._name = config.get(CONF_NAME)
self.host = config.get(CONF_HOST)
self.port = config.get(CONF_PORT)
self.path = config.get(CONF_PATH)
self.user = config.get(CONF_USERNAME)
self.passwd = config.get(CONF_PASSWORD)
self._name = config[CONF_NAME]
self.host = config[CONF_HOST]
self.port = config[CONF_PORT]
self.path = config[CONF_PATH]
self.user = config[CONF_USERNAME]
self.passwd = config[CONF_PASSWORD]
@property
def name(self):
"""Return the name of this camera."""
return self._name
hass.async_add_job(self._connect_to_client)
@property
def brand(self):
"""Camera brand."""
return DEFAULT_BRAND
def get_latest_video_url(self):
@property
def name(self):
"""Return the name of this camera."""
return self._name
async def _connect_to_client(self):
"""Attempt to establish a connection via FTP."""
from aioftp import Client, StatusCodeError
ftp = Client()
try:
await ftp.connect(self.host)
await ftp.login(self.user, self.passwd)
self._ftp = ftp
except StatusCodeError as err:
raise PlatformNotReady(err)
async def _get_latest_video_url(self):
"""Retrieve the latest video file from the customized Yi FTP server."""
from ftplib import FTP, error_perm
ftp = FTP(self.host)
try:
ftp.login(self.user, self.passwd)
except error_perm as exc:
_LOGGER.error('There was an error while logging into the camera')
_LOGGER.debug(exc)
return False
from aioftp import StatusCodeError
try:
ftp.cwd(self.path)
except error_perm as exc:
_LOGGER.error('Unable to find path: %s', self.path)
_LOGGER.debug(exc)
return False
await self._ftp.change_directory(self.path)
dirs = []
for path, attrs in await self._ftp.list():
if attrs['type'] == 'dir' and '.' not in str(path):
dirs.append(path)
latest_dir = dirs[-1]
await self._ftp.change_directory(latest_dir)
dirs = [d for d in ftp.nlst() if '.' not in d]
if not dirs:
_LOGGER.warning("There don't appear to be any uploaded videos")
return False
videos = []
for path, _ in await self._ftp.list():
videos.append(path)
if not videos:
_LOGGER.info('Video folder "%s" empty; delaying', latest_dir)
return None
latest_dir = dirs[-1]
ftp.cwd(latest_dir)
videos = ftp.nlst()
if not videos:
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
return False
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
self.user, self.passwd, self.host, self.port, self.path,
latest_dir, videos[-1])
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)
return None
async def async_camera_image(self):
"""Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG
url = await self.hass.async_add_job(self.get_latest_video_url)
url = await self._get_latest_video_url()
if url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
self._last_image = await asyncio.shield(ffmpeg.get_image(
url, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments), loop=self.hass.loop)
self._last_image = await asyncio.shield(
ffmpeg.get_image(
url,
output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments),
loop=self.hass.loop)
self._last_url = url
return self._last_image
@@ -49,7 +49,6 @@ def _get_image_url(hass, monitor, mode):
@asyncio.coroutine
# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the ZoneMinder cameras."""
cameras = []
@@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "No Google Cast devices found on the network.",
"single_instance_allowed": "Only a single configuration of Google Cast is necessary."
},
"step": {
"confirm": {
"description": "Do you want to setup Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}
+30
View File
@@ -0,0 +1,30 @@
"""Component to embed Google Cast."""
from homeassistant.helpers import config_entry_flow
DOMAIN = 'cast'
REQUIREMENTS = ['pychromecast==2.1.0']
async def async_setup(hass, config):
"""Set up the Cast component."""
hass.data[DOMAIN] = config.get(DOMAIN, {})
return True
async def async_setup_entry(hass, entry):
"""Set up Cast from a config entry."""
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
entry, 'media_player'))
return True
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
from pychromecast.discovery import discover_chromecasts
return await hass.async_add_job(discover_chromecasts)
config_entry_flow.register_discovery_flow(
DOMAIN, 'Google Cast', _async_has_devices)
@@ -0,0 +1,15 @@
{
"config": {
"title": "Google Cast",
"step": {
"confirm": {
"title": "Google Cast",
"description": "Do you want to setup Google Cast?"
}
},
"abort": {
"single_instance_allowed": "Only a single configuration of Google Cast is necessary.",
"no_devices_found": "No Google Cast devices found on the network."
}
}
}
+12 -1
View File
@@ -246,7 +246,8 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
async def async_setup(hass, config):
"""Set up climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component = hass.data[DOMAIN] = \
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config)
async def async_away_mode_set_service(service):
@@ -456,6 +457,16 @@ async def async_setup(hass, config):
return True
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class ClimateDevice(Entity):
"""Representation of a climate device."""
+21 -4
View File
@@ -13,21 +13,27 @@ from homeassistant.components.fritzbox import (
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED)
from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (
ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS)
DEPENDENCIES = ['fritzbox']
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
OPERATION_LIST = [STATE_HEAT, STATE_ECO]
OPERATION_LIST = [STATE_HEAT, STATE_ECO, STATE_OFF, STATE_ON]
MIN_TEMPERATURE = 8
MAX_TEMPERATURE = 28
# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
ON_API_TEMPERATURE = 127.0
OFF_API_TEMPERATURE = 126.5
ON_REPORT_SET_TEMPERATURE = 30.0
OFF_REPORT_SET_TEMPERATURE = 0.0
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Fritzbox smarthome thermostat platform."""
@@ -88,6 +94,9 @@ class FritzboxThermostat(ClimateDevice):
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self._target_temperature in (ON_API_TEMPERATURE,
OFF_API_TEMPERATURE):
return None
return self._target_temperature
def set_temperature(self, **kwargs):
@@ -102,9 +111,13 @@ class FritzboxThermostat(ClimateDevice):
@property
def current_operation(self):
"""Return the current operation mode."""
if self._target_temperature == ON_API_TEMPERATURE:
return STATE_ON
if self._target_temperature == OFF_API_TEMPERATURE:
return STATE_OFF
if self._target_temperature == self._comfort_temperature:
return STATE_HEAT
elif self._target_temperature == self._eco_temperature:
if self._target_temperature == self._eco_temperature:
return STATE_ECO
return STATE_MANUAL
@@ -119,6 +132,10 @@ class FritzboxThermostat(ClimateDevice):
self.set_temperature(temperature=self._comfort_temperature)
elif operation_mode == STATE_ECO:
self.set_temperature(temperature=self._eco_temperature)
elif operation_mode == STATE_OFF:
self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
elif operation_mode == STATE_ON:
self.set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
@property
def min_temp(self):
@@ -14,8 +14,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA,
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
@@ -268,7 +267,8 @@ class GenericThermostat(ClimateDevice):
if self._min_temp:
return self._min_temp
return DEFAULT_MIN_TEMP
# get default temp from super class
return super().min_temp
@property
def max_temp(self):
@@ -277,7 +277,8 @@ class GenericThermostat(ClimateDevice):
if self._max_temp:
return self._max_temp
return DEFAULT_MAX_TEMP
# Get default temp from super class
return super().max_temp
@asyncio.coroutine
def _async_sensor_changed(self, entity_id, old_state, new_state):
-1
View File
@@ -136,7 +136,6 @@ class KNXClimate(ClimateDevice):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
# pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
+28 -4
View File
@@ -17,7 +17,7 @@ from homeassistant.components.climate import (
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO,
ATTR_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
SUPPORT_AUX_HEAT)
SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE)
from homeassistant.components.mqtt import (
@@ -70,6 +70,9 @@ CONF_SWING_MODE_LIST = 'swing_modes'
CONF_INITIAL = 'initial'
CONF_SEND_IF_OFF = 'send_if_off'
CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp'
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
@@ -116,6 +119,10 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float)
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@@ -181,19 +188,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_PAYLOAD_OFF),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE))
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_MIN_TEMP),
config.get(CONF_MAX_TEMP))
])
class MqttClimate(MqttAvailability, ClimateDevice):
"""Representation of a demo climate device."""
"""Representation of an MQTT climate device."""
def __init__(self, hass, name, topic, value_templates, qos, retain,
mode_list, fan_mode_list, swing_mode_list,
target_temperature, away, hold, current_fan_mode,
current_swing_mode, current_operation, aux, send_if_off,
payload_on, payload_off, availability_topic,
payload_available, payload_not_available):
payload_available, payload_not_available,
min_temp, max_temp):
"""Initialize the climate device."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
@@ -219,6 +229,8 @@ class MqttClimate(MqttAvailability, ClimateDevice):
self._send_if_off = send_if_off
self._payload_on = payload_on
self._payload_off = payload_off
self._min_temp = min_temp
self._max_temp = max_temp
@asyncio.coroutine
def async_added_to_hass(self):
@@ -619,3 +631,15 @@ class MqttClimate(MqttAvailability, ClimateDevice):
support |= SUPPORT_AUX_HEAT
return support
@property
def min_temp(self):
"""Return the minimum temperature."""
# pylint: disable=no-member
return self._min_temp
@property
def max_temp(self):
"""Return the maximum temperature."""
# pylint: disable=no-member
return self._max_temp
+12 -6
View File
@@ -32,16 +32,22 @@ NEST_MODE_HEAT_COOL = 'heat-cool'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Nest thermostat."""
if discovery_info is None:
return
"""Set up the Nest thermostat.
No longer in use.
"""
async def async_setup_entry(hass, entry, async_add_devices):
"""Set up the Nest climate device based on a config entry."""
temp_unit = hass.config.units.temperature_unit
all_devices = [NestThermostat(structure, device, temp_unit)
for structure, device in hass.data[DATA_NEST].thermostats()]
thermostats = await hass.async_add_job(hass.data[DATA_NEST].thermostats)
add_devices(all_devices, True)
all_devices = [NestThermostat(structure, device, temp_unit)
for structure, device in thermostats]
async_add_devices(all_devices, True)
class NestThermostat(ClimateDevice):
+4 -4
View File
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
netatmo = hass.components.netatmo
device = config.get(CONF_RELAY)
import lnetatmo
import pyatmo
try:
data = ThermostatData(netatmo.NETATMO_AUTH, device)
for module_name in data.get_module_names():
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
module_name not in config[CONF_THERMOSTAT]:
continue
add_devices([NetatmoThermostat(data, module_name)], True)
except lnetatmo.NoDevice:
except pyatmo.NoDevice:
return None
@@ -168,8 +168,8 @@ class ThermostatData(object):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the NetAtmo API to update the data."""
import lnetatmo
self.thermostatdata = lnetatmo.ThermostatData(self.auth)
import pyatmo
self.thermostatdata = pyatmo.ThermostatData(self.auth)
self.target_temperature = self.thermostatdata.setpoint_temp
self.setpoint_mode = self.thermostatdata.setpoint_mode
self.current_temperature = self.thermostatdata.temp
+3 -3
View File
@@ -19,7 +19,7 @@ from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
SUPPORT_ON_OFF)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -246,13 +246,13 @@ class SensiboClimate(ClimateDevice):
def min_temp(self):
"""Return the minimum temperature."""
return self._temperatures_list[0] \
if self._temperatures_list else DEFAULT_MIN_TEMP
if self._temperatures_list else super().min_temp
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._temperatures_list[-1] \
if self._temperatures_list else DEFAULT_MAX_TEMP
if self._temperatures_list else super().max_temp
@property
def unique_id(self):
+6 -10
View File
@@ -8,8 +8,8 @@ import logging
from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.components.tado import DATA_TADO
@@ -231,18 +231,14 @@ class TadoClimate(ClimateDevice):
@property
def min_temp(self):
"""Return the minimum temperature."""
if self._min_temp:
return self._min_temp
return DEFAULT_MIN_TEMP
return convert_temperature(self._min_temp, self._unit,
self.hass.config.units.temperature_unit)
@property
def max_temp(self):
"""Return the maximum temperature."""
if self._max_temp:
return self._max_temp
return DEFAULT_MAX_TEMP
return convert_temperature(self._max_temp, self._unit,
self.hass.config.units.temperature_unit)
def update(self):
"""Update the state of this climate device."""
+2 -6
View File
@@ -32,8 +32,8 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up of Vera thermostats."""
add_devices_callback(
VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['climate'])
[VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['climate']], True)
class VeraThermostat(VeraDevice, ClimateDevice):
@@ -101,10 +101,6 @@ class VeraThermostat(VeraDevice, ClimateDevice):
if power:
return convert(power, float, 0.0)
def update(self):
"""Handle state updates."""
self._state = self.vera_device.get_hvac_mode()
@property
def temperature_unit(self):
"""Return the unit of measurement."""
-1
View File
@@ -84,7 +84,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([WinkWaterHeater(water_heater, hass)])
# pylint: disable=abstract-method
class WinkThermostat(WinkDevice, ClimateDevice):
"""Representation of a Wink thermostat."""
@@ -0,0 +1,217 @@
"""
Support for ZhongHong HVAC Controller.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.zhong_hong/
"""
import logging
import voluptuous as vol
from homeassistant.components.climate import (
ATTR_OPERATION_MODE, PLATFORM_SCHEMA, STATE_COOL, STATE_DRY,
STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF,
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
from homeassistant.const import (ATTR_TEMPERATURE, CONF_HOST, CONF_PORT,
EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (async_dispatcher_connect,
async_dispatcher_send)
_LOGGER = logging.getLogger(__name__)
CONF_GATEWAY_ADDRRESS = 'gateway_address'
REQUIREMENTS = ['zhong_hong_hvac==1.0.9']
SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added'
SIGNAL_ZHONG_HONG_HUB_START = 'zhong_hong_hub_start'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST):
cv.string,
vol.Optional(CONF_PORT, default=9999):
vol.Coerce(int),
vol.Optional(CONF_GATEWAY_ADDRRESS, default=1):
vol.Coerce(int),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the ZhongHong HVAC platform."""
from zhong_hong_hvac.hub import ZhongHongGateway
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
gw_addr = config.get(CONF_GATEWAY_ADDRRESS)
hub = ZhongHongGateway(host, port, gw_addr)
devices = [
ZhongHongClimate(hub, addr_out, addr_in)
for (addr_out, addr_in) in hub.discovery_ac()
]
_LOGGER.debug("We got %s zhong_hong climate devices", len(devices))
hub_is_initialized = False
async def startup():
"""Start hub socket after all climate entity is setted up."""
nonlocal hub_is_initialized
if not all([device.is_initialized for device in devices]):
return
if hub_is_initialized:
return
_LOGGER.debug("zhong_hong hub start listen event")
await hass.async_add_job(hub.start_listen)
await hass.async_add_job(hub.query_all_status)
hub_is_initialized = True
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADDED, startup)
# add devices after SIGNAL_DEVICE_SETTED_UP event is listend
add_devices(devices)
def stop_listen(event):
"""Stop ZhongHongHub socket."""
hub.stop_listen()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_listen)
class ZhongHongClimate(ClimateDevice):
"""Representation of a ZhongHong controller support HVAC."""
def __init__(self, hub, addr_out, addr_in):
"""Set up the ZhongHong climate devices."""
from zhong_hong_hvac.hvac import HVAC
self._device = HVAC(hub, addr_out, addr_in)
self._hub = hub
self._current_operation = None
self._current_temperature = None
self._target_temperature = None
self._current_fan_mode = None
self._is_on = None
self.is_initialized = False
async def async_added_to_hass(self):
"""Register callbacks."""
self._device.register_update_callback(self._after_update)
self.is_initialized = True
async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADDED)
def _after_update(self, climate):
"""Callback to update state."""
_LOGGER.debug("async update ha state")
if self._device.current_operation:
self._current_operation = self._device.current_operation.lower()
if self._device.current_temperature:
self._current_temperature = self._device.current_temperature
if self._device.current_fan_mode:
self._current_fan_mode = self._device.current_fan_mode
if self._device.target_temperature:
self._target_temperature = self._device.target_temperature
self._is_on = self._device.is_on
self.schedule_update_ha_state()
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def name(self):
"""Return the name of the thermostat, if any."""
return self.unique_id
@property
def unique_id(self):
"""Return the unique ID of the HVAC."""
return "zhong_hong_hvac_{}_{}".format(self._device.addr_out,
self._device.addr_in)
@property
def supported_features(self):
"""Return the list of supported features."""
return (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
| SUPPORT_OPERATION_MODE | SUPPORT_ON_OFF)
@property
def temperature_unit(self):
"""Return the unit of measurement used by the platform."""
return TEMP_CELSIUS
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_operation
@property
def operation_list(self):
"""Return the list of available operation modes."""
return [STATE_COOL, STATE_HEAT, STATE_DRY, STATE_FAN_ONLY]
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
@property
def is_on(self):
"""Return true if on."""
return self._device.is_on
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self._current_fan_mode
@property
def fan_list(self):
"""Return the list of available fan modes."""
return self._device.fan_list
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._device.min_temp
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._device.max_temp
def turn_on(self):
"""Turn on ac."""
return self._device.turn_on()
def turn_off(self):
"""Turn off ac."""
return self._device.turn_off()
def set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is not None:
self._device.set_temperature(temperature)
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
if operation_mode is not None:
self.set_operation_mode(operation_mode)
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
self._device.set_operation_mode(operation_mode.upper())
def set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
self._device.set_fan_mode(fan_mode)
@@ -2,48 +2,85 @@
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.helpers.entity_registry import async_get_registry
from homeassistant.components import websocket_api
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['websocket_api']
WS_TYPE_GET = 'config/entity_registry/get'
SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET,
vol.Required('entity_id'): cv.entity_id
})
WS_TYPE_UPDATE = 'config/entity_registry/update'
SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_UPDATE,
vol.Required('entity_id'): cv.entity_id,
# If passed in, we update value. Passing None will remove old value.
vol.Optional('name'): vol.Any(str, None),
})
async def async_setup(hass):
"""Enable the Entity Registry views."""
hass.http.register_view(ConfigManagerEntityView)
hass.components.websocket_api.async_register_command(
WS_TYPE_GET, websocket_get_entity,
SCHEMA_WS_GET
)
hass.components.websocket_api.async_register_command(
WS_TYPE_UPDATE, websocket_update_entity,
SCHEMA_WS_UPDATE
)
return True
class ConfigManagerEntityView(HomeAssistantView):
"""View to interact with an entity registry entry."""
@callback
def websocket_get_entity(hass, connection, msg):
"""Handle get entity registry entry command.
url = '/api/config/entity_registry/{entity_id}'
name = 'api:config:entity_registry:entity'
async def get(self, request, entity_id):
"""Get the entity registry settings for an entity."""
hass = request.app['hass']
Async friendly.
"""
async def retrieve_entity():
"""Get entity from registry."""
registry = await async_get_registry(hass)
entry = registry.entities.get(entity_id)
entry = registry.entities.get(msg['entity_id'])
if entry is None:
return self.json_message('Entry not found', 404)
connection.send_message_outside(websocket_api.error_message(
msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found'))
return
return self.json(_entry_dict(entry))
connection.send_message_outside(websocket_api.result_message(
msg['id'], _entry_dict(entry)
))
@RequestDataValidator(vol.Schema({
# If passed in, we update value. Passing None will remove old value.
vol.Optional('name'): vol.Any(str, None),
}))
async def post(self, request, entity_id, data):
"""Update the entity registry settings for an entity."""
hass = request.app['hass']
hass.async_add_job(retrieve_entity())
@callback
def websocket_update_entity(hass, connection, msg):
"""Handle get camera thumbnail websocket command.
Async friendly.
"""
async def update_entity():
"""Get entity from registry."""
registry = await async_get_registry(hass)
if entity_id not in registry.entities:
return self.json_message('Entry not found', 404)
if msg['entity_id'] not in registry.entities:
connection.send_message_outside(websocket_api.error_message(
msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found'))
return
entry = registry.async_update_entity(entity_id, **data)
return self.json(_entry_dict(entry))
entry = registry.async_update_entity(
msg['entity_id'], name=msg['name'])
connection.send_message_outside(websocket_api.result_message(
msg['id'], _entry_dict(entry)
))
hass.async_add_job(update_entity())
@callback
-1
View File
@@ -25,7 +25,6 @@ VALUE_TO_STATE = {
}
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 cover platform."""
-2
View File
@@ -107,7 +107,6 @@ class KNXCover(CoverDevice):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
# pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
@@ -197,7 +196,6 @@ class KNXCover(CoverDevice):
@callback
def auto_updater_hook(self, now):
"""Call for the autoupdater."""
# pylint: disable=unused-argument
self.async_schedule_update_ha_state()
if self.device.position_reached():
self.stop_auto_updater()
-1
View File
@@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['lutron']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lutron shades."""
devs = []
@@ -18,7 +18,6 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['lutron_caseta']
# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Lutron Caseta shades as a cover device."""
+1 -1
View File
@@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymyq==0.0.8']
REQUIREMENTS = ['pymyq==0.0.11']
_LOGGER = logging.getLogger(__name__)
@@ -54,7 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the RPi cover platform."""
relay_time = config.get(CONF_RELAY_TIME)
+2 -2
View File
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera covers."""
add_devices(
VeraCover(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['cover'])
[VeraCover(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['cover']], True)
class VeraCover(VeraDevice, CoverDevice):
@@ -23,7 +23,8 @@
"options": {
"title": "Extra configuration options for deCONZ",
"data": {
"allow_clip_sensor": "Allow importing virtual sensors"
"allow_clip_sensor": "Allow importing virtual sensors",
"allow_deconz_groups": "Allow importing deCONZ groups"
}
}
},
@@ -8,7 +8,9 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.helpers import aiohttp_client
from homeassistant.util.json import load_json
from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN
from .const import (
CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN)
CONF_BRIDGEID = 'bridgeid'
@@ -94,12 +96,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
"""Extra options for deCONZ.
CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors.
CONF_DECONZ_GROUPS -- Allow user to choose if they want deCONZ groups.
"""
from pydeconz.utils import async_get_bridgeid
if user_input is not None:
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \
user_input[CONF_ALLOW_CLIP_SENSOR]
self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = \
user_input[CONF_ALLOW_DECONZ_GROUPS]
if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass)
@@ -115,6 +120,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
step_id='options',
data_schema=vol.Schema({
vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool,
vol.Optional(CONF_ALLOW_DECONZ_GROUPS): bool,
}),
)
@@ -158,6 +164,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
return await self.async_step_link()
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True
self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True
return self.async_create_entry(
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
data=self.deconz_config
+1
View File
@@ -10,3 +10,4 @@ DATA_DECONZ_ID = 'deconz_entities'
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
+2 -1
View File
@@ -16,7 +16,8 @@
"options": {
"title": "Extra configuration options for deCONZ",
"data":{
"allow_clip_sensor": "Allow importing virtual sensors"
"allow_clip_sensor": "Allow importing virtual sensors",
"allow_deconz_groups": "Allow importing deCONZ groups"
}
}
},
@@ -31,7 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an Actiontec scanner."""
scanner = ActiontecDeviceScanner(config[DOMAIN])
@@ -30,7 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a Aruba scanner."""
scanner = ArubaDeviceScanner(config[DOMAIN])
@@ -78,7 +78,6 @@ _ARP_REGEX = re.compile(
r'.*')
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an ASUS-WRT scanner."""
scanner = AsusWrtDeviceScanner(config[DOMAIN])
@@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Return a BT Home Hub 5 scanner if successful."""
scanner = BTHomeHub5DeviceScanner(config[DOMAIN])
@@ -27,7 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a DD-WRT scanner."""
try:
@@ -0,0 +1,120 @@
"""
Support for device tracking through Freebox routers.
This tracker keeps track of the devices connected to the configured Freebox.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.freebox/
"""
import asyncio
import copy
import logging
import socket
from collections import namedtuple
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
from homeassistant.const import (
CONF_HOST, CONF_PORT)
REQUIREMENTS = ['aiofreepybox==0.0.3']
_LOGGER = logging.getLogger(__name__)
FREEBOX_CONFIG_FILE = 'freebox.conf'
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port
}))
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the Freebox device tracker and start the polling."""
freebox_config = copy.deepcopy(config)
if discovery_info is not None:
freebox_config[CONF_HOST] = discovery_info['properties']['api_domain']
freebox_config[CONF_PORT] = discovery_info['properties']['https_port']
_LOGGER.info("Discovered Freebox server: %s:%s",
freebox_config[CONF_HOST], freebox_config[CONF_PORT])
scanner = FreeboxDeviceScanner(hass, freebox_config, async_see)
interval = freebox_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
await scanner.async_start(hass, interval)
return True
Device = namedtuple('Device', ['id', 'name', 'ip'])
def _build_device(device_dict):
return Device(
device_dict['l2ident']['id'],
device_dict['primary_name'],
device_dict['l3connectivities'][0]['addr'])
class FreeboxDeviceScanner(object):
"""This class scans for devices connected to the Freebox."""
def __init__(self, hass, config, async_see):
"""Initialize the scanner."""
from aiofreepybox import Freepybox
self.host = config[CONF_HOST]
self.port = config[CONF_PORT]
self.token_file = hass.config.path(FREEBOX_CONFIG_FILE)
self.async_see = async_see
# Hardcode the app description to avoid invalidating the authentication
# file at each new version.
# The version can be changed if we want the user to re-authorize HASS
# on her Freebox.
app_desc = {
'app_id': 'hass',
'app_name': 'Home Assistant',
'app_version': '0.65',
'device_name': socket.gethostname()
}
api_version = 'v1' # Use the lowest working version.
self.fbx = Freepybox(
app_desc=app_desc,
token_file=self.token_file,
api_version=api_version)
async def async_start(self, hass, interval):
"""Perform a first update and start polling at the given interval."""
await self.async_update_info()
interval = max(interval, MIN_TIME_BETWEEN_SCANS)
async_track_time_interval(hass, self.async_update_info, interval)
async def async_update_info(self, now=None):
"""Check the Freebox for devices."""
from aiofreepybox.exceptions import HttpRequestError
_LOGGER.info('Scanning devices')
await self.fbx.open(self.host, self.port)
try:
hosts = await self.fbx.lan.get_hosts_list()
except HttpRequestError:
_LOGGER.exception('Failed to scan devices')
else:
active_devices = [_build_device(device)
for device in hosts
if device['active']]
if active_devices:
await asyncio.wait([self.async_see(mac=d.id, host_name=d.name)
for d in active_devices])
await self.fbx.close()
@@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a HUAWEI scanner."""
scanner = HuaweiDeviceScanner(config[DOMAIN])
@@ -23,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Return a Sky Hub scanner if successful."""
scanner = SkyHubDeviceScanner(config[DOMAIN])
@@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an SNMP scanner."""
scanner = SnmpScanner(config[DOMAIN])
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a THOMSON scanner."""
scanner = ThomsonDeviceScanner(config[DOMAIN])
@@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a Unifi direct scanner."""
scanner = UnifiDeviceScanner(config[DOMAIN])
@@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
})
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
def get_scanner(hass, config):
+3 -2
View File
@@ -46,7 +46,9 @@ SERVICE_HOMEKIT = 'homekit'
CONFIG_ENTRY_HANDLERS = {
SERVICE_DECONZ: 'deconz',
'google_cast': 'cast',
SERVICE_HUE: 'hue',
'sonos': 'sonos',
}
SERVICE_HANDLERS = {
@@ -64,11 +66,9 @@ SERVICE_HANDLERS = {
SERVICE_SABNZBD: ('sabnzbd', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
SERVICE_KONNECTED: ('konnected', None),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'),
'sonos': ('media_player', 'sonos'),
'yamaha': ('media_player', 'yamaha'),
'logitech_mediaserver': ('media_player', 'squeezebox'),
'directv': ('media_player', 'directv'),
@@ -84,6 +84,7 @@ SERVICE_HANDLERS = {
'kodi': ('media_player', 'kodi'),
'volumio': ('media_player', 'volumio'),
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
'freebox': ('device_tracker', 'freebox'),
}
OPTIONAL_SERVICE_HANDLERS = {
+121 -38
View File
@@ -4,14 +4,16 @@ Support for DoorBird device.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/doorbird/
"""
import asyncio
import logging
import asyncio
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import CONF_HOST, CONF_USERNAME, \
CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
REQUIREMENTS = ['DoorBirdPy==0.1.3']
@@ -24,60 +26,139 @@ API_URL = '/api/{}'.format(DOMAIN)
CONF_DOORBELL_EVENTS = 'doorbell_events'
CONF_CUSTOM_URL = 'hass_url_override'
DOORBELL_EVENT = 'doorbell'
MOTION_EVENT = 'motionsensor'
# Sensor types: Name, device_class, event
SENSOR_TYPES = {
'doorbell': ['Button', 'occupancy', DOORBELL_EVENT],
'motion': ['Motion', 'motion', MOTION_EVENT],
}
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CUSTOM_URL): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
vol.Optional(CONF_CUSTOM_URL): cv.string,
})
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])
}),
}, extra=vol.ALLOW_EXTRA)
SENSOR_DOORBELL = 'doorbell'
def setup(hass, config):
"""Set up the DoorBird component."""
from doorbirdpy import DoorBird
device_ip = config[DOMAIN].get(CONF_HOST)
username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD)
# Provide an endpoint for the doorstations to call to trigger events
hass.http.register_view(DoorbirdRequestView())
device = DoorBird(device_ip, username, password)
status = device.ready()
doorstations = []
if status[0]:
_LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username)
hass.data[DOMAIN] = device
elif status[1] == 401:
_LOGGER.error("Authorization rejected by DoorBird at %s", device_ip)
return False
else:
_LOGGER.error("Could not connect to DoorBird at %s: Error %s",
device_ip, str(status[1]))
return False
for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]):
device_ip = doorstation_config.get(CONF_HOST)
username = doorstation_config.get(CONF_USERNAME)
password = doorstation_config.get(CONF_PASSWORD)
custom_url = doorstation_config.get(CONF_CUSTOM_URL)
events = doorstation_config.get(CONF_MONITORED_CONDITIONS)
name = (doorstation_config.get(CONF_NAME)
or 'DoorBird {}'.format(index + 1))
if config[DOMAIN].get(CONF_DOORBELL_EVENTS):
# Provide an endpoint for the device to call to trigger events
hass.http.register_view(DoorbirdRequestView())
device = DoorBird(device_ip, username, password)
status = device.ready()
if status[0]:
_LOGGER.info("Connected to DoorBird at %s as %s", device_ip,
username)
doorstation = ConfiguredDoorbird(device, name, events, custom_url)
doorstations.append(doorstation)
elif status[1] == 401:
_LOGGER.error("Authorization rejected by DoorBird at %s",
device_ip)
return False
else:
_LOGGER.error("Could not connect to DoorBird at %s: Error %s",
device_ip, str(status[1]))
return False
# SETUP EVENT SUBSCRIBERS
if events is not None:
# This will make HA the only service that receives events.
doorstation.device.reset_notifications()
# Subscribe to doorbell or motion events
subscribe_events(hass, doorstation)
hass.data[DOMAIN] = doorstations
return True
def subscribe_events(hass, doorstation):
"""Initialize the subscriber."""
for sensor_type in doorstation.monitored_events:
name = '{} {}'.format(doorstation.name,
SENSOR_TYPES[sensor_type][0])
event_type = SENSOR_TYPES[sensor_type][2]
# Get the URL of this server
hass_url = hass.config.api.base_url
# Override it if another is specified in the component configuration
if config[DOMAIN].get(CONF_CUSTOM_URL):
hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
_LOGGER.info("DoorBird will connect to this instance via %s",
hass_url)
# Override url if another is specified onth configuration
if doorstation.custom_url is not None:
hass_url = doorstation.custom_url
# This will make HA the only service that gets doorbell events
url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
device.reset_notifications()
device.subscribe_notification(SENSOR_DOORBELL, url)
slug = slugify(name)
return True
url = '{}{}/{}'.format(hass_url, API_URL, slug)
_LOGGER.info("DoorBird will connect to this instance via %s",
url)
_LOGGER.info("You may use the following event name for automations"
": %s_%s", DOMAIN, slug)
doorstation.device.subscribe_notification(event_type, url)
class ConfiguredDoorbird():
"""Attach additional information to pass along with configured device."""
def __init__(self, device, name, events=None, custom_url=None):
"""Initialize configured device."""
self._name = name
self._device = device
self._custom_url = custom_url
self._monitored_events = events
@property
def name(self):
"""Custom device name."""
return self._name
@property
def device(self):
"""The configured device."""
return self._device
@property
def custom_url(self):
"""Custom url for device."""
return self._custom_url
@property
def monitored_events(self):
"""Get monitored events."""
if self._monitored_events is None:
return []
return self._monitored_events
class DoorbirdRequestView(HomeAssistantView):
@@ -93,5 +174,7 @@ class DoorbirdRequestView(HomeAssistantView):
def get(self, request, sensor):
"""Respond to requests from the device."""
hass = request.app['hass']
hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor))
return 'OK'
+1 -2
View File
@@ -48,7 +48,6 @@ def request_configuration(network, hass, config):
return
# pylint: disable=unused-argument
def ecobee_configuration_callback(callback_data):
"""Handle configuration callbacks."""
network.request_tokens()
@@ -106,7 +105,7 @@ def setup(hass, config):
Will automatically load thermostat and sensor components to support
devices discovered on the network.
"""
# pylint: disable=global-statement, import-error
# pylint: disable=import-error
global NETWORK
if 'ecobee' in _CONFIGURING:
+15 -23
View File
@@ -4,7 +4,6 @@ Support for Eight smart mattress covers and mattresses.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/eight_sleep/
"""
import asyncio
import logging
from datetime import timedelta
@@ -22,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
REQUIREMENTS = ['pyeight==0.0.8']
REQUIREMENTS = ['pyeight==0.0.9']
_LOGGER = logging.getLogger(__name__)
@@ -86,8 +85,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the Eight Sleep component."""
from pyeight.eight import EightSleep
@@ -107,31 +105,29 @@ def async_setup(hass, config):
hass.data[DATA_EIGHT] = eight
# Authenticate, build sensors
success = yield from eight.start()
success = await eight.start()
if not success:
# Authentication failed, cannot continue
return False
@asyncio.coroutine
def async_update_heat_data(now):
async def async_update_heat_data(now):
"""Update heat data from eight in HEAT_SCAN_INTERVAL."""
yield from eight.update_device_data()
await eight.update_device_data()
async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT)
async_track_point_in_utc_time(
hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL)
@asyncio.coroutine
def async_update_user_data(now):
async def async_update_user_data(now):
"""Update user data from eight in USER_SCAN_INTERVAL."""
yield from eight.update_user_data()
await eight.update_user_data()
async_dispatcher_send(hass, SIGNAL_UPDATE_USER)
async_track_point_in_utc_time(
hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL)
yield from async_update_heat_data(None)
yield from async_update_user_data(None)
await async_update_heat_data(None)
await async_update_user_data(None)
# Load sub components
sensors = []
@@ -157,8 +153,7 @@ def async_setup(hass, config):
CONF_BINARY_SENSORS: binary_sensors,
}, config))
@asyncio.coroutine
def async_service_handler(service):
async def async_service_handler(service):
"""Handle eight sleep service calls."""
params = service.data.copy()
@@ -170,7 +165,7 @@ def async_setup(hass, config):
side = sens.split('_')[1]
userid = eight.fetch_userid(side)
usrobj = eight.users[userid]
yield from usrobj.set_heating_level(target, duration)
await usrobj.set_heating_level(target, duration)
async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT)
@@ -179,10 +174,9 @@ def async_setup(hass, config):
DOMAIN, SERVICE_HEAT_SET, async_service_handler,
schema=SERVICE_EIGHT_SCHEMA)
@asyncio.coroutine
def stop_eight(event):
async def stop_eight(event):
"""Handle stopping eight api session."""
yield from eight.stop()
await eight.stop()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight)
@@ -196,8 +190,7 @@ class EightSleepUserEntity(Entity):
"""Initialize the data object."""
self._eight = eight
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_eight_user_update():
@@ -220,8 +213,7 @@ class EightSleepHeatEntity(Entity):
"""Initialize the data object."""
self._eight = eight
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_eight_heat_update():
-1
View File
@@ -13,7 +13,6 @@ FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
LIMITED_SUPPORT = SUPPORT_SET_SPEED
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up the demo fan platform."""
add_devices_callback([
-1
View File
@@ -30,7 +30,6 @@ for key in VALUE_TO_STATE:
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 fan platform."""
+1 -2
View File
@@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'zhimi.humidifier.ca1']),
})
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
ATTR_MODEL = 'model'
@@ -314,7 +314,6 @@ SERVICE_TO_METHOD = {
}
# pylint: disable=unused-argument
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the miio fan device from config."""
+112 -201
View File
@@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/frontend/
"""
import asyncio
import hashlib
import json
import logging
import os
@@ -24,14 +23,13 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
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==20180608.0b0']
REQUIREMENTS = ['home-assistant-frontend==20180616.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
@@ -99,9 +97,22 @@ WS_TYPE_GET_PANELS = 'get_panels'
SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_PANELS,
})
WS_TYPE_GET_THEMES = 'frontend/get_themes'
SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_THEMES,
})
WS_TYPE_GET_TRANSLATIONS = 'frontend/get_translations'
SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_TRANSLATIONS,
vol.Required('language'): str,
})
WS_TYPE_GET_EXPERIMENTAL_UI = 'frontend/experimental_ui'
SCHEMA_GET_EXPERIMENTAL_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_EXPERIMENTAL_UI,
})
class AbstractPanel:
class Panel:
"""Abstract class for panels."""
# Name of the webcomponent
@@ -113,44 +124,12 @@ class AbstractPanel:
# Title to show in the sidebar (optional)
sidebar_title = None
# Url to the webcomponent (depending on JS version)
webcomponent_url_es5 = None
webcomponent_url_latest = None
# Url to show the panel in the frontend
frontend_url_path = None
# Config to pass to the webcomponent
config = None
@asyncio.coroutine
def async_register(self, hass):
"""Register panel with HASS."""
panels = hass.data.get(DATA_PANELS)
if panels is None:
panels = hass.data[DATA_PANELS] = {}
if self.frontend_url_path in panels:
_LOGGER.warning("Overwriting component %s", self.frontend_url_path)
if DATA_FINALIZE_PANEL in hass.data:
yield from hass.data[DATA_FINALIZE_PANEL](self)
panels[self.frontend_url_path] = self
@callback
def async_register_index_routes(self, router, index_view):
"""Register routes for panel to be served by index view."""
router.add_route(
'get', '/{}'.format(self.frontend_url_path), index_view.get)
router.add_route(
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get)
class BuiltInPanel(AbstractPanel):
"""Panel that is part of hass_frontend."""
def __init__(self, component_name, sidebar_title, sidebar_icon,
frontend_url_path, config):
"""Initialize a built-in panel."""
@@ -160,6 +139,16 @@ class BuiltInPanel(AbstractPanel):
self.frontend_url_path = frontend_url_path or component_name
self.config = config
@callback
def async_register_index_routes(self, router, index_view):
"""Register routes for panel to be served by index view."""
router.add_route(
'get', '/{}'.format(self.frontend_url_path), index_view.get)
router.add_route(
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get)
@callback
def to_response(self, hass, request):
"""Panel as dictionary."""
return {
@@ -171,95 +160,25 @@ class BuiltInPanel(AbstractPanel):
}
class ExternalPanel(AbstractPanel):
"""Panel that is added by a custom component."""
REGISTERED_COMPONENTS = set()
def __init__(self, component_name, path, md5, sidebar_title, sidebar_icon,
frontend_url_path, config):
"""Initialize an external panel."""
self.component_name = component_name
self.path = path
self.md5 = md5
self.sidebar_title = sidebar_title
self.sidebar_icon = sidebar_icon
self.frontend_url_path = frontend_url_path or component_name
self.config = config
@asyncio.coroutine
def async_finalize(self, hass, frontend_repository_path):
"""Finalize this panel for usage.
frontend_repository_path is set, will be prepended to path of built-in
components.
"""
try:
if self.md5 is None:
self.md5 = yield from hass.async_add_job(
_fingerprint, self.path)
except OSError:
_LOGGER.error('Cannot find or access %s at %s',
self.component_name, self.path)
hass.data[DATA_PANELS].pop(self.frontend_url_path)
return
self.webcomponent_url_es5 = self.webcomponent_url_latest = \
URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5)
if self.component_name not in self.REGISTERED_COMPONENTS:
hass.http.register_static_path(
self.webcomponent_url_latest, self.path,
# if path is None, we're in prod mode, so cache static assets
frontend_repository_path is None)
self.REGISTERED_COMPONENTS.add(self.component_name)
def to_response(self, hass, request):
"""Panel as dictionary."""
result = {
'component_name': self.component_name,
'icon': self.sidebar_icon,
'title': self.sidebar_title,
'url_path': self.frontend_url_path,
'config': self.config,
}
if _is_latest(hass.data[DATA_JS_VERSION], request):
result['url'] = self.webcomponent_url_latest
else:
result['url'] = self.webcomponent_url_es5
return result
@bind_hass
@asyncio.coroutine
def async_register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, frontend_url_path=None,
config=None):
async def async_register_built_in_panel(hass, component_name,
sidebar_title=None, sidebar_icon=None,
frontend_url_path=None, config=None):
"""Register a built-in panel."""
panel = BuiltInPanel(component_name, sidebar_title, sidebar_icon,
frontend_url_path, config)
yield from panel.async_register(hass)
panel = Panel(component_name, sidebar_title, sidebar_icon,
frontend_url_path, config)
panels = hass.data.get(DATA_PANELS)
if panels is None:
panels = hass.data[DATA_PANELS] = {}
@bind_hass
@asyncio.coroutine
def async_register_panel(hass, component_name, path, md5=None,
sidebar_title=None, sidebar_icon=None,
frontend_url_path=None, config=None):
"""Register a panel for the frontend.
if panel.frontend_url_path in panels:
_LOGGER.warning("Overwriting component %s", panel.frontend_url_path)
component_name: name of the web component
path: path to the HTML of the web component
(required unless url is provided)
md5: the md5 hash of the web component (for versioning in URL, optional)
sidebar_title: title to show in the sidebar (optional)
sidebar_icon: icon to show next to title in sidebar (optional)
url_path: name to use in the URL (defaults to component_name)
config: config to be passed into the web component
"""
panel = ExternalPanel(component_name, path, md5, sidebar_title,
sidebar_icon, frontend_url_path, config)
yield from panel.async_register(hass)
if DATA_FINALIZE_PANEL in hass.data:
hass.data[DATA_FINALIZE_PANEL](panel)
panels[panel.frontend_url_path] = panel
@bind_hass
@@ -278,11 +197,10 @@ def add_manifest_json_key(key, val):
MANIFEST_JSON[key] = val
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the serving of the frontend."""
if list(hass.auth.async_auth_providers):
client = yield from hass.auth.async_create_client(
client = await hass.auth.async_create_client(
'Home Assistant Frontend',
redirect_uris=['/'],
no_secret=True,
@@ -291,7 +209,15 @@ def async_setup(hass, config):
client = None
hass.components.websocket_api.async_register_command(
WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS)
WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS)
hass.components.websocket_api.async_register_command(
WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES)
hass.components.websocket_api.async_register_command(
WS_TYPE_GET_TRANSLATIONS, websocket_get_translations,
SCHEMA_GET_TRANSLATIONS)
hass.components.websocket_api.async_register_command(
WS_TYPE_GET_EXPERIMENTAL_UI, websocket_experimental_config,
SCHEMA_GET_EXPERIMENTAL_UI)
hass.http.register_view(ManifestJSONView)
conf = config.get(DOMAIN, {})
@@ -331,24 +257,23 @@ def async_setup(hass, config):
index_view = IndexView(repo_path, js_version, client)
hass.http.register_view(index_view)
async def finalize_panel(panel):
@callback
def async_finalize_panel(panel):
"""Finalize setup of a panel."""
if hasattr(panel, 'async_finalize'):
await panel.async_finalize(hass, repo_path)
panel.async_register_index_routes(hass.http.app.router, index_view)
yield from asyncio.wait([
async_register_built_in_panel(hass, panel)
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template', 'dev-mqtt', 'kiosk')], loop=hass.loop)
await asyncio.wait(
[async_register_built_in_panel(hass, panel) for panel in (
'dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template', 'dev-mqtt', 'kiosk', 'experimental-ui')],
loop=hass.loop)
hass.data[DATA_FINALIZE_PANEL] = finalize_panel
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
# Finalize registration of panels that registered before frontend was setup
# This includes the built-in panels from line above.
yield from asyncio.wait(
[finalize_panel(panel) for panel in hass.data[DATA_PANELS].values()],
loop=hass.loop)
for panel in hass.data[DATA_PANELS].values():
async_finalize_panel(panel)
if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set()
@@ -360,16 +285,14 @@ def async_setup(hass, config):
for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []):
add_extra_html_url(hass, url, True)
async_setup_themes(hass, conf.get(CONF_THEMES))
hass.http.register_view(TranslationsView)
_async_setup_themes(hass, conf.get(CONF_THEMES))
return True
def async_setup_themes(hass, themes):
@callback
def _async_setup_themes(hass, themes):
"""Set up themes data and services."""
hass.http.register_view(ThemesView)
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
if themes is None:
hass.data[DATA_THEMES] = {}
@@ -456,38 +379,23 @@ class IndexView(HomeAssistantView):
return tpl
@asyncio.coroutine
def get(self, request, extra=None):
async def get(self, request, extra=None):
"""Serve the index view."""
hass = request.app['hass']
latest = self.repo_path is not None or \
_is_latest(self.js_option, request)
if request.path == '/':
panel = 'states'
else:
panel = request.path.split('/')[1]
if panel == 'states':
panel_url = ''
elif latest:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest
else:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
no_auth = '1'
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
# do not try to auto connect on load
no_auth = '0'
template = yield from hass.async_add_job(self.get_template, latest)
template = await hass.async_add_job(self.get_template, latest)
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
template_params = dict(
no_auth=no_auth,
panel_url=panel_url,
panels=hass.data[DATA_PANELS],
theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[extra_key],
)
@@ -506,54 +414,13 @@ class ManifestJSONView(HomeAssistantView):
url = '/manifest.json'
name = 'manifestjson'
@asyncio.coroutine
@callback
def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json."""
msg = json.dumps(MANIFEST_JSON, sort_keys=True)
return web.Response(text=msg, content_type="application/manifest+json")
class ThemesView(HomeAssistantView):
"""View to return defined themes."""
requires_auth = False
url = '/api/themes'
name = 'api:themes'
@callback
def get(self, request):
"""Return themes."""
hass = request.app['hass']
return self.json({
'themes': hass.data[DATA_THEMES],
'default_theme': hass.data[DATA_DEFAULT_THEME],
})
class TranslationsView(HomeAssistantView):
"""View to return backend defined translations."""
url = '/api/translations/{language}'
name = 'api:translations'
@asyncio.coroutine
def get(self, request, language):
"""Return translations."""
hass = request.app['hass']
resources = yield from async_get_translations(hass, language)
return self.json({
'resources': resources,
})
def _fingerprint(path):
"""Fingerprint a file."""
with open(path) as fil:
return hashlib.md5(fil.read().encode('utf-8')).hexdigest()
def _is_latest(js_option, request):
"""
Return whether we should serve latest untranspiled code.
@@ -587,7 +454,7 @@ def _is_latest(js_option, request):
@callback
def websocket_handle_get_panels(hass, connection, msg):
def websocket_get_panels(hass, connection, msg):
"""Handle get panels command.
Async friendly.
@@ -600,3 +467,47 @@ def websocket_handle_get_panels(hass, connection, msg):
connection.to_write.put_nowait(websocket_api.result_message(
msg['id'], panels))
@callback
def websocket_get_themes(hass, connection, msg):
"""Handle get themes command.
Async friendly.
"""
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], {
'themes': hass.data[DATA_THEMES],
'default_theme': hass.data[DATA_DEFAULT_THEME],
}))
@callback
def websocket_get_translations(hass, connection, msg):
"""Handle get translations command.
Async friendly.
"""
async def send_translations():
"""Send a camera still."""
resources = await async_get_translations(hass, msg['language'])
connection.send_message_outside(websocket_api.result_message(
msg['id'], {
'resources': resources,
}
))
hass.async_add_job(send_translations())
def websocket_experimental_config(hass, connection, msg):
"""Send experimental UI config over websocket config."""
async def send_exp_config():
"""Send experimental frontend config."""
config = await hass.async_add_job(
load_yaml, hass.config.path('experimental-ui.yaml'))
connection.send_message_outside(websocket_api.result_message(
msg['id'], config
))
hass.async_add_job(send_exp_config())
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

+10 -4
View File
@@ -171,14 +171,20 @@ def async_setup(hass, config):
development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO)
if development_repo is not None:
hass.http.register_static_path(
'/api/hassio/app-es5',
os.path.join(development_repo, 'hassio/build-es5'), False)
'/api/hassio/app',
os.path.join(development_repo, 'hassio/build'), False)
hass.http.register_view(HassIOView(host, websession))
if 'frontend' in hass.config.components:
yield from hass.components.frontend.async_register_built_in_panel(
'hassio', 'Hass.io', 'hass:home-assistant')
yield from hass.components.panel_custom.async_register_panel(
frontend_url_path='hassio',
webcomponent_name='hassio-main',
sidebar_title='Hass.io',
sidebar_icon='hass:home-assistant',
js_url='/api/hassio/app/entrypoint.js',
embed_iframe=True,
)
if 'http' in config:
yield from hassio.update_hass_api(config['http'])
@@ -37,6 +37,7 @@ def homekit_http_send(self, message_body=None, encode_chunked=False):
Appends an extra \r\n to the buffer.
A message_body may be specified, to be appended to the request.
"""
# pylint: disable=protected-access
self._buffer.extend((b"", b""))
msg = b"\r\n".join(self._buffer)
del self._buffer[:]

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