Compare commits

...

129 Commits

Author SHA1 Message Date
Paulus Schoutsen 1afdde61e8 Merge pull request #9395 from home-assistant/release-0-53-1
0.53.1
2017-09-11 22:43:56 -07:00
viswa-swami 2a8620f806 Fixing foscam library dependency/requirements (#9387)
* Added support to enable/disable motion detection for foscam cameras. This support was added in 0.48.1 as a generic service for cameras. Motion detection can be enabled/disabled for foscam cameras with this code-set.

* Fixed the violation identified by hound-bot

* Fixed the comment posted by HoundCI-Bot regarding using imperative mood statement for pydocstyle

* Fixed the error that travis-ci bot found.

* As per comment from @balloob, Instead of directly using the URL to talk to foscam, used a 3rd party foscam library to communicate with it. This library already has support to enable/disable motion detection and also APIs to change the motion detection schedule etc. Need to add more support in the pyfoscam 3rd party library for checking if motion was detected or even if sound was detected. Once that is done, we can add that into HASS as well.

* Lint

* Removed the requests library import which is not used anymore

* Updating requirements_all.txt based on the code-base of home assistant that i have. Generated using the gen_requirements_all.py script

* Updating requirements_all.txt and requirements_test_all.txt generated by gen_requirements_all.py after latest pull from origin/dev

* Updated requirements_all.txt with script

* Updated the foscam camera code to fix lint errors

* Fixed houndci violation

* Updating the foscam library dependency/requirements.

* Fixing the requirements_all file. Somehow when i generated, it generated duplicate entry for the same dependency
2017-09-11 21:53:20 -07:00
Alok Saboo 804d06d0d3 Fixes #9379 - Added additional string check in Wunderground sensor (#9380)
* Added additional string check

* optimaze
2017-09-11 21:53:20 -07:00
Mike Christianson 202d4d8105 Fixes #9353 (#9354)
Follow [Twitter's guidance](https://dev.twitter.com/rest/reference/post/media/upload-finalize) for media uploads: "If and (only if) the response of the FINALIZE command contains a processing_info field, it may also be necessary to use a STATUS command and wait for it to return success before proceeding to Tweet creation."
2017-09-11 21:53:19 -07:00
Paulus Schoutsen 04dccb4246 Version bump to 0.53.1 2017-09-11 21:53:01 -07:00
Paulus Schoutsen 7f5c4cd1e5 Update frontend 2017-09-11 21:52:48 -07:00
Paulus Schoutsen 3f2eba0932 Version bump to 0.53 2017-09-09 00:51:52 -07:00
Paulus Schoutsen 2d72cff575 Merge pull request #9327 from home-assistant/release-0-53
0.53
2017-09-09 00:31:53 -07:00
John Mihalic 3065575777 Bump pyHik version to add IO support (#9341) 2017-09-09 00:06:57 -07:00
Paulus Schoutsen 74bfcde814 Cleanup input_text (#9326) 2017-09-09 00:06:57 -07:00
Aaron Bach c539b5c12b Adds the AirVisual air quality sensor platform (#9320)
* Adds the AirVisual air quality sensor platform

* Updated .coveragerc

* Removed some un-needed code

* Adding strangely-necessary pylint disable

* Removing a Python3.5-specific dict combiner method

* Restarting stuck coverage test

* Added units to AQI sensor (to get nice graph)

* Making collaborator-requested changes

* Removing unnecessary parameter from data object
2017-09-09 00:06:56 -07:00
Sergey Isachenko d2d876945b Fix for potential issue with tesla initialization (#9307)
Fix for potential issue with tesla initialization
2017-09-09 00:06:56 -07:00
Paulus Schoutsen 7036a7845c Update frontend 2017-09-08 23:08:58 -07:00
Paulus Schoutsen fc7ffba9ae Merge branch 'master' into release-0-53 2017-09-08 21:52:29 -07:00
Julius Mittenzwei 77d0ad1797 Stable and asynchronous KNX library. (#8725)
* First draft of XKNX module for Home-Assistant

* XKNX does now take path of xknx.yaml as parameter

* small fix, telegram_received_callback has different signature

* changed method of registering callbacks of devices

* removed non async command lines from xknx

* telegram_received_cb not needed within HASS module

* updated requirements

* Configuration if XKNX should connect via Routing or Tunneling

* bumping version to 0.6.1

* small fix within xknx plugin

* bumped version

* XKNX-Switches are now BinarySensors and Logic from Sensor was moved to BinarySensor

* renamed Outlet to Switch

* pylint

* configuration of KNX lights via HASS config, yay!

* changed name of attribute

* Added configuration for xknx to switch component

* added support for sensors within hass configuration

* added support for climate within hass configuration

* Thermostat -> Climate

* added configuration support for binary_sensors

* renamed Shutter to Cover

* added configuration support for cover

* restructured file structure according to HASS requirements

* pylint

* pylint

* pylint

* pylint

* pylint

* pylint

* updated version

* pylint

* pylint

* pylint

* added setpoint support for climate devices

* devices are now in a different module

* more asyncio :-)

* pydocstyle

* pydocstyle

* added actions to binary_sensor

* allow more than one automation

* readded requirement

* Modifications suggested by hound

* Modifications suggested by hound

* Modifications suggested by hound

* Modifications suggested by hound

* xknx now imported as local import

* hound *sigh*

* lint

* 'fixed' coverage.

* next try for getting gen_requirements_all.py working

* removed blank line

* XKNX 0.7.1 with logging functionality, replaced some print() calls with _LOGGER

* updated requirements_all.txt

* Fixes issue https://github.com/XKNX/xknx/issues/51

* https://github.com/XKNX/xknx/issues/52 added raw access to KNX bus from HASS component.

* bumped version - 0.7.3 contains some bugfixes

* bumped version - 0.7.3 contains some bugfixes

* setting setpoint within climate device has to be async

* bumped version to 0.7.4

* bumped version

* https://github.com/XKNX/xknx/issues/48 Adding HVAC support.

* pylint suggestions

* Made target temperature and set point required attributes

* renamed value_type to type within sensor configuration

* Issue https://github.com/XKNX/xknx/issues/52 : added filter functionality for not flooding the event bus.

* suggestions by pylint

* Added notify support for knx platform.

* logging error if discovery_info is None.

* review suggestions by @armills

* line too long

* Using discovery_info to notifiy component which devices should be added.

* moved XKNX automation to main level.

* renamed xknx component to knx.

* reverted change within .coveragerc

* changed dependency

* updated docstrings.

* updated version of xknx within requirements_all.txt

* moved requirement to correct position

* renamed configuration attribute

* added @callback-decorator and async_prefix.

* added @callback decorator and async_ prefix to register_callbacks functions

* fixed typo

* pylint suggestions

* added angle position and invert_position and invert_angle to cover.knx

* typo

* bumped version within requirements_all.txt

* bumped version

* Added support for HVAC controller status
2017-09-07 00:11:55 -07:00
Sebastian Muszynski 9a7089bad3 Platform not ready behavior fixed. (#9325)
Expose the device model as sensor attribute.
Device initialized log message added. Provides device model, firmware and hardware version.
2017-09-07 00:01:59 -07:00
Mister Wil 894200d87d Fixed bug with devices not being discovered correctly. (#9311) 2017-09-06 09:11:32 -07:00
Alok Saboo fad914de8c Version bump dlib to 1.0.0 (#9316) 2017-09-06 07:35:34 -07:00
ohmer1 5971a7c009 Optionally disable ssl certificate validity check. (#9181)
* Optionally disable ssl certificate validity check.

* Fix lines too long.

* Fix formatting.

* Force build CI

* Fix "Method could be a function (no-self-use)"
2017-09-06 08:58:13 +03:00
Mike Christianson e7a5f7bcdf Follow Twitter guidelines for media upload by conforming to the "STATUS" phase, when required, and by providing "media_category" information. These will, for example, allow users to upload videos that exceed the basic 30 second limit. (#9261)
See:
 - https://twittercommunity.com/t/media-category-values/64781/7
 - https://twittercommunity.com/t/duration-too-long-maximim-30000/68760
 - https://dev.twitter.com/rest/reference/get/media/upload-status.html
2017-09-05 18:49:40 -07:00
Konstantin Belyalov 9ade8002ac Add new config variable to MQTT light (#9304)
* Add new config variable to MQTT light

* Address reviewer's issues: refactor template render part.

* Update mqtt.py
2017-09-06 01:01:03 +02:00
Joe Lu 788275da32 Add post_pending_state attribute to manual alarm_control_panel (#9291)
Add post_pending_state attribute to manual alarm_control_panel
2017-09-05 20:26:59 +02:00
Erik Eriksson 418ccc820a Handle the case where no registration number is available (instead display VIN (vehicle identification number)). (#9073) 2017-09-05 09:10:01 -07:00
Jan Almeroth e4bb8b0444 Introducing a media_player component for Yamaha Multicast devices (#9258)
* Introducing media_player yamaha_multicast

* Fix pep8_max_line_length

* Revert "Fix pep8_max_line_length"

This reverts commit 664c25d657.

* Revert "Introducing media_player yamaha_multicast"

This reverts commit a4fb64b53a.

* Introducing media_player for Yamaha MultiCast Devices

* Add missing Docstrings

* Adding Requirements

* Add Geofency device tracker (#9106)

* Added Geofency device tracker

Added Geofency device tracker

* fix pylint error

* review fixes

* merge coroutines

* Version bump

* Version bump

* D210: No whitespaces allowed surrounding docstring text

* Fix linting

* Version bump

* Revert "Add Geofency device tracker (#9106)"

This reverts commit c240d907d2.

* Fix Invalid method names

* Fix update_status timer

* Fix Invalid class name "mcDevice"

* Fix Access to a protected members

* Introducing source_list setter

* Fix logging

* Version bump

* D400: First line should end with a period (not 'e')

* Removed unnecessary logging

* Minor changes

Thanks to comments from @andrey-git
2017-09-05 19:07:58 +03:00
BioSehnsucht 552abf7da5 Add input_text component (#9112) 2017-09-05 09:04:07 -07:00
runningman84 9ede0f57e6 Added DWD WarnApp Sensor (#8657)
* Added DWD WarnApp Sensor

* Fixed some idents and spaces

* Removed unused imports

* Removed comment

* Some fixes

* Added throttle

* Renamed sensor to dwd weather warnings

* Renamed test file

* shorten lines

* shorten lines

* Implemented changes requested by fabaff

* added ATTRIBUTION

* move ATTRIBUTION to existing method

* fixed lint tests

* Fix linter issues

* Fix linter issues

* Fix linter

* Fixed linter
2017-09-05 08:40:47 -07:00
Phil Cole 0b1677de6d Expose hue group 0 (#8663)
* Tado Fix #8606

Handle case where 'mode' and 'fanSpeed' are missing JSON. Based on
changes in commit
https://github.com/wmalgadey/tado_component/commit/adfb608f86b8bf4c1c43e71b4067cbfe1de9ba85

* Expose hue group 0 to HA #8652

If allow_hue_groups is set expose "All Hue Lights" group for "special
group 0".  This does add an additional Hue API call for every refresh
(approx 30 secs) to get the status of the special group 0 because it's
not included in the full API pull that currently occurs.

* Revert "Expose hue group 0 to HA #8652"

This reverts commit db7fe47ec7.

* Expose hue group 0 to HA #8652

If allow_hue_groups is set expose "All Hue Lights" group for "special
group 0".  This does add an additional Hue API call for every refresh
(approx 30 secs) to get the status of the special group 0 because it's
not included in the full API pull that currently occurs.

* Changes per review by balloob

1) Use all_lights instead of all_lamps
2) Fix line lengths and trailing whitespace
3) Move "All Hue Lights" to GROUP_NAME_ALL_HUE_LIGHTS constant

* Make "All Hue Lights" a constant

* Fix trailing whitespace
2017-09-05 08:38:12 -07:00
Sean Gollschewsky 968ed6ef5b Ensure display-name does not exceed 12 characters for CecAdapter. (#9268)
* Ensure display-name does not exceed 12 characters for CecAdapter.

* Miscalculated offset.
2017-09-05 18:11:02 +03:00
Pascal Vizeli a28ac37a91 Update jinja to 2.9.6 (#9306)
* Update jinja 2.10

* Update requirements_all.txt

* Update package_constraints.txt

* Update package_constraints.txt

* Update requirements_all.txt

* Update setup.py
2017-09-05 17:03:24 +02:00
Dan Sarginson 5ba39c849e Fix for Honeywell Round thermostats (#9308)
This fixes an issue (#8554) whereby the Honeywell thermostats stopped
working after a period of hours or days. We do this by forgetting the
authorisation token that was sent back to us when we first logged in,
which causes the underlying evohomeclient library to perform the full
login procedure again.
2017-09-05 07:06:28 -04:00
Brian Hopkins 984cae5310 Upgrade mycroftapi to 2.0 (#9309)
* updating mycroftapi version

* updating mycroftapi version
2017-09-05 07:05:31 -04:00
upsert c3a91000ac Improved Lutron Caseta shade support (#9302) 2017-09-05 11:30:36 +02:00
Pascal Vizeli ed699896cb Core track same state for a period / Allow on platforms (#9273)
* Core track state period / Allow on platforms

* Add tests

* fix lint

* fix tests

* add new tracker to automation state

* update schema

* fix bug

* revert validate string

* Fix bug

* Set arguments to async_check_funct

* add logic into numeric_state

* fix numeric_state

* Add tests

* fix retrigger state

* cleanup

* Add delay function to template binary_sensor

* Fix tests & lint

* add more tests

* fix lint

* Address comments

* fix test & lint
2017-09-05 02:01:01 +02:00
Tom Matheussen 67828cb7a2 Handle spotify failing to refresh access_token (#9295)
* Handle spotify failing to refresh access_token

* Remove whitespace
2017-09-04 20:47:40 +02:00
Andreas Jacobsen 54de3d89d1 Added intent_type to exception log (#9289) 2017-09-04 13:40:08 +02:00
Jeroen ter Heerdt 1b5e574a76 Fixing bug when using egardiaserver - package requirement updated to 1.0.20. (#9294)
* Bumping pythonegardia package requirement up to .18

* Updating requirements_all to reflect updated pythonegardia package .18

* Catching up with reality and updating egardia.py

Requirements_all reflects updated package requirement for python-egardia of 1.0.20
2017-09-04 13:34:56 +02:00
Daniel Høyer Iversen e6207684bf rfxtrx lib upgrade (#9288) 2017-09-04 10:19:58 +02:00
Fabian Affolter 7c7a5a4a15 Upgrade python-telegram-bot to 8.0.0 (#9282) 2017-09-03 17:21:51 -04:00
Fabian Affolter 5dfd60a029 Upgrade youtube_dl to 2017.9.2 (#9279) 2017-09-03 17:21:35 -04:00
Paul Sokolovsky 38e1b81ff6 discovery: If unknown NetDisco service discovered, log about it. (#9280)
Otherwise, known services are logged, ignored are logged, but unknown -
not. Logging them is quite helpful for someone working on adding new
discovery service to NetDisco/HA, and would help to decouple NetDisco
library further: another project may use a generic NetDisco library,
and contribute new service to it, which won't be automatically supported
by HA. But logging about it would be a good hint to HA users that they
can look into supporting it.
2017-09-03 16:27:13 -04:00
Dan Ports 68343ac81f insteon_plm: fix typo in attributes (#9284) 2017-09-03 15:42:05 -04:00
emlt 7694c31814 Change attribute names (#9277)
Remove spaces and capitals in attribute names to be consistent with sensors and other switches.
2017-09-03 16:07:12 +02:00
Greg Dowling db36b5cd23 Merge pull request #9274 from home-assistant/bump_pywemo
Bump pywemo, handle more ports.
2017-09-03 12:13:10 +01:00
pavoni a78f5e0970 Bump pywemo, handle more ports. 2017-09-03 11:31:55 +01:00
Abílio Costa 0889e38cb1 flux: fix for when stop_time is after midnight (#8932)
* flux: fix for when stop_time is after midnight

* flux: fix imports

* flux: add missing check when now is after midnight

* flux: one more try; should fix all use cases now

* flux switch: fix lint

* flux switch: add new tests

* flux switch: fix tests lint

* flux switch: fix tests docstrings
2017-09-02 18:02:11 +02:00
Gunnar Helgason f51163f803 Add Geofency device tracker (#9106)
* Added Geofency device tracker

Added Geofency device tracker

* fix pylint error

* review fixes

* merge coroutines
2017-09-01 23:56:59 +02:00
Matthew Breedlove 639eb81aef Adding ZWave CentralScene activation handler. (#9178)
* Adding ZWave CentralScene activation handler.

* Migrated CentralScene logic to node_entity.py

Removed extraneous logging

Modified scene_activated event to send the scene_id and scene_data separately

* Adding unit test for ZWave central scene activation

* Removed return to allow node statistics to update after central scene message is received
2017-09-01 21:41:35 +02:00
Fabian Affolter 8797932f80 Upgrade psutil to 5.3.0 (#9253) 2017-09-01 18:05:53 +02:00
Fabian Affolter 8d1f6d3995 Upgrade sendgrid to 5.2.0 (#9254) 2017-09-01 18:05:37 +02:00
Christian Brædstrup 4defd96cd6 Version bump of DLink switch to v0.6.0 (#9252) 2017-09-01 15:27:43 +02:00
snjoetw 185d838803 This is to fix #6386: Manual Alarm not re-arm after 2nd trigger (#9249) 2017-09-01 12:08:30 +02:00
Philipp Schmitt 713f7fa2a1 Fix nello.io login (#9251) 2017-09-01 12:02:22 +02:00
Daniel Høyer Iversen 4cd5173ac8 upgrade xiaomi lib (#9250) 2017-09-01 11:58:26 +02:00
Oliver 8d5f6723ce Added configurable timeout for receiver HTTP requests | Additional AV… (#9244)
* Added configurable timeout for receiver HTTP requests | Additional AVR-X detection based on CommApiVers | Treat Marantz SR6007 - SR6010 as AVR-X device

* timeout value not passed correctly
2017-09-01 09:15:47 +02:00
Marcelo Moreira de Mello a55895b662 Make sure Ring binary_sensor state will update only if device_id matches (#9247) 2017-09-01 09:14:16 +02:00
Pascal Vizeli 0af4f8903d Add available to sonos (#9243)
* Readd sonos available flag / fix polling state

* cleanup
2017-09-01 00:23:11 +02:00
Pascal Vizeli 836b528bd3 WIP: Homematic improvments with new hass interfaces (#9058)
* Remove hass to init hack and use official interfaces

* fix lint

* Fix lint

* change style
2017-08-31 21:16:44 +02:00
Martin Hjelmare 274e4449ea Fix possible KeyError (#9242)
* Multiple devices per child per platform would lead to KeyError.
2017-08-31 21:00:09 +02:00
Daniel Høyer Iversen acb6b7c68d title and message was swapped in pushbullet (#9241) 2017-08-31 20:41:22 +02:00
Adam Mills 7d281fd224 Skip automatic events older than latest data (#9230)
* Skip automatic events older than latest data

* Update test
2017-08-31 16:29:18 +02:00
Fabian Affolter 60342b4738 Upgrade discord.py to 0.16.11 (#9239) 2017-08-31 16:26:52 +02:00
happyleavesaoc 99c1c9472a mopar sensor (#9136)
* mopar sensor

* fix doc url

* mopar review comments

* remove unneeded hass.data handling

* fix lint
2017-08-31 16:26:33 +02:00
Daniel Høyer Iversen d816ff26ad A bugfix for pushbullet (#9237)
* Bug fix for pushbullet
2017-08-31 14:19:33 +02:00
John K. Luebs e22ec28bce Use ZCL mandatory attribute to determine ZHA light capabilities (#9232)
The manadatory ColorCapabilities attribute should indicate whether a
light is capable of XY color changes and/or color temperature.
2017-08-31 00:18:01 -05:00
Andrey bb37294047 Allow panels with external URL (#9214)
* Allow panels with external URL

* Update comment
2017-08-30 23:21:24 -05:00
Maciej Sokołowski de4a4fe71a [light.tradfri] Full range of white spectrum lightbulbs support (#9224)
* [light.tradfri] Support for pytradfri version supporting full white spectrum

* [light.tradfri] Checkout pytradfri master

* Developer docker image adjusted

* [light.tradfri] pytradfri 2.2 support for white spectrum bulbs

* Removed fix already included in dev

* Style adjusted

* pylint false positive overriden

* Review remarks applied (#1)

* make pylint happy

* Review remarks
2017-08-30 23:19:06 -05:00
Sergey Isachenko 5f445b4a13 Tesla platform (#9211)
* Tesla support implemetation

* requirements_all.txt fix

* .coveragerc fix

* logging-too-many-args fix

* logging-too-many-args attempt 2

* Post-review fixes.

* requirements version fix

* requirements

* Lint fix

* Hot fix

* requirements_all.txt fix

* Review preparation.

* 1. Linting fix.
2. Minimal value for SCAN_INTERVAL hardcoded to 300 sec (to prevent possible ban form Tesla)

* Removed redundant whitespace.

* Fixed components according to @MartinHjelmare proposals and remarks.

* .coveragerc as @MartinHjelmare suggested.

* Minor changes

* Fix docstrings

* Update ordering

* Update quotes

* Minor changes

* Update quotes
2017-08-30 23:13:02 -05:00
Fabian Affolter 10e8aea46b Upgrade shodan to 1.7.5 (#9228) 2017-08-30 22:23:28 +02:00
Kris Molendyke 76c7eef7d8 Add Tank Utility sensor (#9132)
* Add Tank Utility sensor

* Fix, disable Pylint errors

* Move coverage omission to single platform section

* Do not catch unknown exceptions

* Check for invalid credentials in setup

* Update tank_utility.py
2017-08-30 22:21:54 +02:00
Daniel Høyer Iversen 214c92d787 pushbullet, send a file from url (#9189)
* pushbullet, send a file from url

* pushbullet, send a file from url

* Simplify
2017-08-30 21:42:27 +02:00
Jeroen ter Heerdt f2551c08af Egardia package to .19 and change in port number for egardiaserver (#9225) 2017-08-30 20:11:45 +02:00
Lukas Barth 3a0e38aa73 Add max_age to statistics sensor (#8790)
* Add max_age to statistics sensor

* Allow only non-zero sampling sizes

* Fix long line

* Fix style
2017-08-30 17:13:36 +02:00
Riccardo Canta 56f9ccb877 Allow sonos to select album as a source (#9221)
Importing the fix in the PR https://github.com/home-assistant/home-assistant/pull/8258 I noticed that the same error is present also for Spotify album so I have extended the code and tested it. It works fine on my setup
2017-08-30 15:10:02 +02:00
Marcelo Moreira de Mello f76436f326 Fix fitbit error when trying to access token after upgrade. (#9183)
*   - Fixes Fitbit error when trying to refresh oauth token

  The 3rd python-fitbit module requires an extra kwarg on the FitBit
  constructor called refresh_cb. The value should be a function that
  accepts one argument token.

  This value will be a dictionary with the keys:

     'access_token', 'refresh_token', 'expires_at'

  This implements a lambda refresh_cb as required by the Fitbit module
  to work, however the new token will always be save manually on
  every update() call.

*  Simplified by calling  expires_at instead reading again from dict
2017-08-30 10:01:01 +02:00
Fabian Affolter 4aafcfa478 Upgrade sendgrid to 5.0.1 (#9215) 2017-08-29 21:06:31 -07:00
Fabian Affolter 8673e53940 Upgrade pyasn1 to 0.3.3 and pyasn1-modules to 0.1.1 (#9216) 2017-08-29 21:06:18 -07:00
Nicholas Sielicki ebc7ade591 directv: extended discovery via REST api, bug fix (#8800)
* fix not providing device for discovered directvs

This fixes a bug introduced at 6884965c80

Discovered directv boxes would not be instantiated with a DEVICE
parameter.

Signed-off-by: Nicholas Sielicki <sielicki@yandex.com>

* directv: add discovery of RVU clients

If discovery is used with directv, also try to further discover and
configure RVU client set-top boxes by requesting information from a REST
service running on the main directv box/RVU-server.

This commit also disables discovery if any directv configuration is
supplied by the user.

Signed-off-by: Nicholas Sielicki <sielicki@yandex.com>

* components/media_player/directv.py: use hass.data

Use hass.data instead of a global to remember state.

Signed-off-by: Nicholas Sielicki <sielicki@yandex.com>

* unconditionally import requests in directv.py

Requests is a core requirement, so we're okay to import at the top of
the file rather than conditionally / in a function.

Signed-off-by: Nicholas Sielicki <sielicki@yandex.com>
2017-08-30 00:08:56 +02:00
Jeff McGehee 7de73e9ef7 Bayesian Binary Sensor (#8810)
* Bayesian Binary Sensor

Why:

* It would be beneficial to leverage various sensor outputs in a
Bayesian manner in order to sense more complex events.

This change addresses the need by:

* `BayesianBinarySensor` class in
`./homeassistant/components/binary_sensor/bayesian.py`
* Tests in `./tests/components/binary_sensor/test_bayesian.py`

Caveats:
This is my first time in this code-base. I did try to follow conventions
that I was able to find, but I'm sure there will be some issues to
straighten out.

* minor cleanup

* Address reviewer's comments

This change addresses the need by:

* Removing `CONF_SENSOR_CLASS` and its usage in `get_deprecated`.
* Make probability update function a static method, and use single `_`
to match project conventions.

* Address linter failures

* fix `device_class` declaration

* Address Comments

Why:
* Not validating config schema enough.
* Not following common practices for async initialization.
* Naive implementation of Bayes' rule.

This change addresses the need by:
* Improving config validation for observations.
* Moving initialization logic into `async_added_to_hass`.
* Re-configuring Bayesian updates to allow true P|Q usage.

* address linting issues

* Improve DRYness by adding `_update_current_obs` method

* update doc strings and ensure functions are set up properly for async

* Make only 1 state change handle

* fix style

* fix style part 2

* fix lint
2017-08-29 23:53:41 +02:00
Paulus Schoutsen 0b58d5405e Add cloud auth support (#9208)
* Add initial cloud auth

* Move hass.data to a dict

* Move mode into helper

* Fix bugs afte refactor

* Add tests

* Clean up scripts file after test config

* Lint

* Update __init__.py
2017-08-29 13:40:08 -07:00
Mister Wil 33c906c20a Abode push events and lock, cover, and switch components (#9095)
* Updated abodepy version to 0.7.1

* Refactored to use AbodeDevice. Added Abode Lock device.

* Added push updates to abode devices.

* Upgraded to 0.7.2 after finding issue with callbacks.

* Refactored to use AbodeDevice. Added Abode Lock device.

* Added push updates to abode devices.

* Upgraded to 0.7.2 after finding issue with callbacks.

* Bumped version to 0.8.2. Modified code to work with new constants and properties. Added cover and switch.

* Fixed hound violations.

* Updated to 0.8.3 to fix small bug with standby mode. Fixed comment in cover/abode.py.

* Fix lint issues

* Removed excessive logging. Moved device callback registration to async_added_to_hass. Moved abode controller from global into hass data.

* Removed explicit None from dict.get()

* Move device class into the constructor.

* Changed constant name to platforms.

* Changes as requested.

* Removing stray blank line.

* Added blank line of which I'm not sure how it was removed.

* Updated version to 0.9.0. Fixed motion sensor. Added power_switch_meter device type.

* Update abode.py

* fix lint
2017-08-29 17:34:19 +02:00
Paulus Schoutsen 81a00bf3f1 Lint Sonarr tests 2017-08-29 08:10:38 -07:00
Martin Hjelmare b8d737c0cc Upgrade pymysensors to 0.11.1 (#9212) 2017-08-29 17:10:28 +02:00
Daniel Høyer Iversen ee28b439b3 Refactor rfxtrx (#9117)
* rfxtrx refactor

* rfxtrx refactor

* rfxtrx refactor

* rfxtrx refactor

* rfxtrx refactor

* rfxtrx refactor

* rfxtrx refactor

* rfxtrx refactor
2017-08-29 16:22:28 +02:00
Daniel Høyer Iversen aa8dd8fbdd Issue #6893 in rfxtrx (#9130)
* Issue #6893 in rfxtrx

* Update rfxtrx.py

* rfxtrx issue
2017-08-29 16:20:26 +02:00
William Scanlon 3e0eb8763f Support for season sensor (#8958)
Add an optional extended description…
2017-08-29 16:18:36 +02:00
Fabian Affolter 0687a457b1 Add counter component (#9146) 2017-08-29 15:44:36 +02:00
Dale Higgs 38071501b4 Fix and optimize digitalloggers platform (#9203)
* Fix and optimize digitalloggers platform

* Fix line length

* Fix hanging indentation

* Add missing docstring

* Add period to end of docstring

* Add second blank line
2017-08-29 15:38:42 +02:00
mjj4791 5d800c1d51 Prevent error when no forecast data was available (#9176)
* Prevent error when no forecast data was available

Prevent an Error when buienradar data was available, but no forecasted data was retrieved for the requested day.

* Update buienradar.py

* Update buienradar.py
2017-08-29 15:33:47 +02:00
Trevor 75559cb81f Add "status" to Sonarr sensor (#9204)
* Use X-Api-Key header

* Increase timeout

* Add "status" to Sonarr sensor

* Update test_sonarr.py

* Update test_sonarr.py

* Update test_sonarr.py

* Update sonarr.py

* Update sonarr.py
2017-08-29 15:33:27 +02:00
aetolus 0de6a37822 fix worldtidesinfo #9184 (#9201) 2017-08-29 08:28:40 +02:00
bobnwk 6505019701 Update pushbullet.py (#9200) 2017-08-29 05:40:33 +02:00
Mario Wenzel e76e9e0966 Fix dht22 when no data was read initially #8976 (#9198)
This fixes https://github.com/home-assistant/home-assistant/issues/8976
When no data was available the module crashes.
2017-08-28 22:46:31 +03:00
Paulus Schoutsen bd71a33ba8 Merge pull request #9196 from home-assistant/release-0-52-1
0.52.1
2017-08-28 09:22:00 -07:00
Nolan Gilley 0ccff6c03e bump ecobee version to fix issue 9190 (#9191) 2017-08-28 09:15:34 -07:00
mjj4791 3509ecf07f Prevent iCloud exceptions in logfile (#9179)
* Prevent iCloud exceptions in logfile

With this change ValueError exceptions in the logfile caused by this component will disappear.
These errors are caused by the iCloud API returning an HTTP 450 error and the external lib throwing a ValueError because of it.

A PR has been raised against the external library, but that fix did not yet make it into a new version of the library. This will catch the exception in the mean time.... https://github.com/picklepete/pyicloud/pull/138

* Align log messages
2017-08-28 09:15:34 -07:00
Paulus Schoutsen 308b822832 Wrap state when iterating a domain in templates (#9157) 2017-08-28 09:15:34 -07:00
Adam Mills d986b8f4c2 Bump aioautomatic to prevent leaking exceptions (#9148) 2017-08-28 09:15:33 -07:00
Sean Dague e6892a4077 Fix import for foscam (#9140)
While waiting for a new pyfoscam release, we can fix this for users
just by changing the import. Foscam devices a pretty widely deployed,
so a regression here is definitely no fun.

Fixes Bug #8940
2017-08-28 09:15:33 -07:00
Daniel Høyer Iversen 422be25d22 bug fix pushbullet (#9139) 2017-08-28 09:15:33 -07:00
Daniel Høyer Iversen 0ae1f85f9f Fix issue #9116 in pushbullet (#9128)
* Fix issue #9116 in pushbullet
2017-08-28 09:15:32 -07:00
Andrey Kupreychik 8a89643338 Close stream request once we end up with proxy (#9110)
* Close stream request once we end up with proxy

* Update aiohttp_client.py

* Update aiohttp_client.py

* Removed trailing whitespace
2017-08-28 09:15:32 -07:00
Paulus Schoutsen 10e3c00f07 Version bump to 0.52.1 2017-08-28 09:11:11 -07:00
mjj4791 cc18b5af3d Prevent iCloud exceptions in logfile (#9179)
* Prevent iCloud exceptions in logfile

With this change ValueError exceptions in the logfile caused by this component will disappear.
These errors are caused by the iCloud API returning an HTTP 450 error and the external lib throwing a ValueError because of it.

A PR has been raised against the external library, but that fix did not yet make it into a new version of the library. This will catch the exception in the mean time.... https://github.com/picklepete/pyicloud/pull/138

* Align log messages
2017-08-28 09:09:36 -07:00
Paulus Schoutsen 924290adb0 Update frontend 2017-08-28 09:04:34 -07:00
Nolan Gilley f9c22b0e61 bump ecobee version to fix issue 9190 (#9191) 2017-08-28 10:12:21 -05:00
Ryan Kraus 2533b49aef Merge pull request #9182 from home-assistant/pyisy-update
Bumped the version of PyISY
2017-08-28 00:02:28 -04:00
Ryan Kraus f6a701e843 Bumped the version of PyISY
PyISY has been updated to better support newer ISY994 firmware. This
should resolve #7601.
2017-08-27 23:24:29 -04:00
Brian Hopkins bd039b8c53 Mycroft notify/component (#9173)
* working mycroft notification platform

* Update mycroft.py

* Update mycroft.py

* Update mycroft.py

* Update mycroft.py

* updating to use new api

updating code to use new api.

* updating changes

updating files

* updating typos

fixing some typos

* Update mycroft.py

adding text

* fixing pep issues

fixing pep issues

* adding new mycroft component

adding mycroft component

* updating

updating code

* updating typo

fixing typo

* updating file

adding updates

* updating notify

updating notify component for new changes

* Update mycroft.py

* Update mycroft.py

* Update mycroft.py

* updating for tox

updating to pass tox tests

* updating for tox

fixing tox errors

* fixing tox issues

fixing tox issues

* fixing tox issues

fixing more tox issues

* updating requirement

adding requirement for component

* fixed typo

fixed typo

* updating requirements

updating requirements

* updating code

updating code

* updating files

updating

* updating

* adding logging

adding in logging

* fixing typo

fixing typo

* updating debugs

* updating files

updating files

* updating dependencies

updating dependencies

* updating to load notification

updating to load notification

* cleaning up whitespace

* updating requirements_all.txt

* adding requirement

adding requirement

* Update mycroft.py

* Update .coveragerc

updated .coveragerc
2017-08-27 13:53:20 -07:00
Sebastian Muszynski de48d42f33 "TypeError: write_to_hub() takes 2 positional arguments but 4 were given" fixed. (#9174) 2017-08-27 21:41:47 +02:00
Sebastian Muszynski bf315da8df Xiaomi gateway: Device support for the Aqara Water Leak Sensor (sensor_wleak.aq1) (#9172)
* Device support for the Aqara Water Leak Sensor (sensor_wleak.aq1) added.

* Required version of PyXiaomiGateway changed.
2017-08-27 21:06:11 +02:00
EmitKiwi 654f6892f9 Mysensors nodes can be renamed in config file (#9123)
* Mysensors nodes can be renamed in the config file

* Replace nodes array with dict. Replace whole name of the node.

* Improved iteration on node names
2017-08-27 20:40:38 +02:00
Adam Mills cd3f0f8f96 Use node_modules gulp in script/build_frontend (#9170) 2017-08-27 13:46:37 -04:00
Daniel Høyer Iversen 499d54c8fc upgrade xiaomi lib to 0.3.1 to supprt water sensor (#9168) 2017-08-27 19:31:34 +02:00
Paulus Schoutsen 5629157740 Allow getting number of available states in template (#9158) 2017-08-27 18:33:25 +02:00
Andrey c367021aa4 Allow specifying custom html urls to load. (#9150)
* Allow specifying custom html urls to load.

* Change add_extra_html_urls to accept a single URL
2017-08-27 09:07:58 -07:00
Fabian Affolter 8fdd9712e6 Upgrade uber_rides to 0.5.2 (#9149) 2017-08-27 11:31:06 +02:00
Fabian Affolter f47de06f02 Upgrade sphinx-autodoc-typehints to 1.2.3 (#9151) 2017-08-27 11:30:42 +02:00
Fabian Affolter 7062c2b257 Remove links to gitter (#9155) 2017-08-27 11:30:26 +02:00
Fabian Affolter ae5fca1ec9 Upgrade async_timeout to 1.3.0 (#9156) 2017-08-27 11:30:04 +02:00
Paulus Schoutsen 8605098ea0 Wrap state when iterating a domain in templates (#9157) 2017-08-26 17:00:59 -07:00
Adam Mills 21bf089b17 Bump aioautomatic to prevent leaking exceptions (#9148) 2017-08-26 17:09:57 -04:00
Andrey c73338bf3e Backend changes for customize config panel. (#9134)
* Backend changes for customize config panel.

* Backend changes for customize config panel.

* Add customize.yaml to default config

* Precreate customize.yaml

* Add tests
2017-08-26 10:02:32 -07:00
Andrey Kupreychik c537770786 Close stream request once we end up with proxy (#9110)
* Close stream request once we end up with proxy

* Update aiohttp_client.py

* Update aiohttp_client.py

* Removed trailing whitespace
2017-08-26 09:56:39 -07:00
Daniel Høyer Iversen 493353e4de bug fix pushbullet (#9139) 2017-08-26 09:12:51 -07:00
Sean Dague f4d464c008 Fix import for foscam (#9140)
While waiting for a new pyfoscam release, we can fix this for users
just by changing the import. Foscam devices a pretty widely deployed,
so a regression here is definitely no fun.

Fixes Bug #8940
2017-08-26 09:08:37 -07:00
Daniel Høyer Iversen 0d3fa59d77 Fix issue #9116 in pushbullet (#9128)
* Fix issue #9116 in pushbullet
2017-08-26 09:36:54 +02:00
Sebastian Muszynski 56083c0c64 Xiaomi Philips Lights integration (#9087)
* Adds support for the Xiaomi Philips LED Ball and Ceiling Lamp

* Documentation url updated.

* New component to .coveragerc added.

* Unused import removed.

* translate labeled as static method.

* Mixed parameters in log message fixed.

* Order of requirements_all.txt fixed.

* Plattform updated. It's async now.

* Simplifiable if-statement fixed.

* Some more clean-up of unneeded stuff.

* Platform schema updated.

* Component is called xiaomi_philipslight now.

* Requirements all updated.

* Initialization of some variables updated.

* Raise PlatformNotReady exception if light cannot be discovered.

* Import of math removed.
Missing space added.

* Remove unnecessary updates
2017-08-25 21:27:31 -07:00
Martin Hjelmare 8775c54d29 Refactor mysensors callback and add validation (#9069)
* Refactor mysensors callback and add validation

* Add mysensors entity class. The mysensors entity class inherits from
  a more general mysensors device class.
* Extract mysensors name function.
* Add setup_mysensors_platform for mysensors platforms.
* Add mysensors const schemas.
* Update mysensors callback and add child validation.
* Remove gateway wrapper class.
* Add better logging for mysensors callback.
* Add discover_persistent_devices function.
* Remove discovery in mysensors component setup.
* Clean up gateway storage in hass.data.
* Update all mysensors platforms.
  * Add repr for MySensorsNotificationDevice.
  * Fix bug in mysensors climate target temperatures.
  * Clean up platforms. Child validation simplifies assumptions in
    platforms.
  * Remove not needed try except statements. All messages are validated
    already in pymysensors.
* Clean up logging.
* Add timer debug logging if callback is slow.
* Upgrade pymysensors to 0.11.0.

* Make dispatch callback async

* Pass tuple device_args and optional add_devices

* Also return new_devices as list instead of dictionary.
2017-08-25 08:58:05 -07:00
Paulus Schoutsen 044b96e3cd Version bump to 0.53.0.dev0 2017-08-25 08:44:35 -07:00
173 changed files with 8282 additions and 1580 deletions
+11
View File
@@ -170,6 +170,9 @@ omit =
homeassistant/components/tellstick.py
homeassistant/components/*/tellstick.py
homeassistant/components/tesla.py
homeassistant/components/*/tesla.py
homeassistant/components/*/thinkingcleaner.py
homeassistant/components/tradfri.py
@@ -328,6 +331,7 @@ omit =
homeassistant/components/light/tplink.py
homeassistant/components/light/tradfri.py
homeassistant/components/light/x10.py
homeassistant/components/light/xiaomi_philipslight.py
homeassistant/components/light/yeelight.py
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py
@@ -380,6 +384,8 @@ omit =
homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/yamaha_musiccast.py
homeassistant/components/mycroft.py
homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py
@@ -397,6 +403,7 @@ omit =
homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py
homeassistant/components/notify/nfandroidtv.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/prowl.py
@@ -420,6 +427,7 @@ omit =
homeassistant/components/remote/itach.py
homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/airvisual.py
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py
@@ -445,6 +453,7 @@ omit =
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/dublin_bus_transport.py
homeassistant/components/sensor/dwd_weather_warnings.py
homeassistant/components/sensor/ebox.py
homeassistant/components/sensor/eddystone_temperature.py
homeassistant/components/sensor/eliqonline.py
@@ -480,6 +489,7 @@ omit =
homeassistant/components/sensor/metoffice.py
homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/modem_callerid.py
homeassistant/components/sensor/mopar.py
homeassistant/components/sensor/mqtt_room.py
homeassistant/components/sensor/mvglive.py
homeassistant/components/sensor/netdata.py
@@ -517,6 +527,7 @@ omit =
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/synologydsm.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/tank_utility.py
homeassistant/components/sensor/ted5000.py
homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py
-4
View File
@@ -33,10 +33,6 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://discord.gg/c5DvZ4e
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://img.shields.io/badge/gitter-general-blue.svg
:target: https://gitter.im/home-assistant/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| image:: https://img.shields.io/badge/gitter-development-yellowgreen.svg
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
:target: https://home-assistant.io/demo/
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
+6
View File
@@ -101,6 +101,12 @@ def reload_core_config(hass):
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
@asyncio.coroutine
def async_reload_core_config(hass):
"""Reload the core config."""
yield from hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up general services related to Home Assistant."""
+69 -18
View File
@@ -4,15 +4,20 @@ This component provides basic support for Abode Home Security system.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/abode/
"""
import asyncio
import logging
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import discovery
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME
from homeassistant.helpers.entity import Entity
from homeassistant.const import (ATTR_ATTRIBUTION,
CONF_USERNAME, CONF_PASSWORD,
CONF_NAME, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
REQUIREMENTS = ['abodepy==0.7.1']
REQUIREMENTS = ['abodepy==0.9.0']
_LOGGER = logging.getLogger(__name__)
@@ -20,8 +25,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com"
DOMAIN = 'abode'
DEFAULT_NAME = 'Abode'
DATA_ABODE = 'data_abode'
DEFAULT_ENTITY_NAMESPACE = 'abode'
DATA_ABODE = 'abode'
NOTIFICATION_ID = 'abode_notification'
NOTIFICATION_TITLE = 'Abode Security Setup'
@@ -34,19 +38,22 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
ABODE_PLATFORMS = [
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover'
]
def setup(hass, config):
"""Set up Abode component."""
import abodepy
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
try:
data = AbodeData(username, password)
hass.data[DATA_ABODE] = data
for component in ['binary_sensor', 'alarm_control_panel']:
discovery.load_platform(hass, component, DOMAIN, {}, config)
hass.data[DATA_ABODE] = abode = abodepy.Abode(
username, password, auto_login=True, get_devices=True)
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
@@ -58,18 +65,62 @@ def setup(hass, config):
notification_id=NOTIFICATION_ID)
return False
for platform in ABODE_PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
def logout(event):
"""Logout of Abode."""
abode.stop_listener()
abode.logout()
_LOGGER.info("Logged out of Abode")
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout)
def startup(event):
"""Listen for push events."""
abode.start_listener()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup)
return True
class AbodeData:
"""Shared Abode data."""
class AbodeDevice(Entity):
"""Representation of an Abode device."""
def __init__(self, username, password):
"""Initialize Abode oject."""
import abodepy
def __init__(self, controller, device):
"""Initialize a sensor for Abode device."""
self._controller = controller
self._device = device
self.abode = abodepy.Abode(username, password)
self.devices = self.abode.get_devices()
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe Abode events."""
self.hass.async_add_job(
self._controller.register, self._device,
self._update_callback
)
_LOGGER.debug("Abode Security set up with %s devices",
len(self.devices))
@property
def should_poll(self):
"""Return the polling state."""
return False
@property
def name(self):
"""Return the name of the sensor."""
return self._device.name
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'device_id': self._device.device_id,
'battery_low': self._device.battery_low,
'no_response': self._device.no_response
}
def _update_callback(self, device):
"""Update the device state."""
self.schedule_update_ha_state()
@@ -6,10 +6,12 @@ https://home-assistant.io/components/alarm_control_panel.abode/
"""
import logging
from homeassistant.components.abode import (DATA_ABODE, DEFAULT_NAME)
from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
from homeassistant.components.abode import (
AbodeDevice, DATA_ABODE, DEFAULT_NAME, CONF_ATTRIBUTION)
from homeassistant.components.alarm_control_panel import (AlarmControlPanel)
from homeassistant.const import (ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
import homeassistant.components.alarm_control_panel as alarm
DEPENDENCIES = ['abode']
@@ -20,30 +22,19 @@ ICON = 'mdi:security'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for an Abode device."""
data = hass.data.get(DATA_ABODE)
abode = hass.data[DATA_ABODE]
add_devices([AbodeAlarm(hass, data, data.abode.get_alarm())])
add_devices([AbodeAlarm(abode, abode.get_alarm())])
class AbodeAlarm(alarm.AlarmControlPanel):
class AbodeAlarm(AbodeDevice, AlarmControlPanel):
"""An alarm_control_panel implementation for Abode."""
def __init__(self, hass, data, device):
def __init__(self, controller, device):
"""Initialize the alarm control panel."""
super(AbodeAlarm, self).__init__()
self._device = device
AbodeDevice.__init__(self, controller, device)
self._name = "{0}".format(DEFAULT_NAME)
@property
def should_poll(self):
"""Return the polling state."""
return True
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def icon(self):
"""Return icon."""
@@ -52,11 +43,11 @@ class AbodeAlarm(alarm.AlarmControlPanel):
@property
def state(self):
"""Return the state of the device."""
if self._device.mode == "standby":
if self._device.is_standby:
state = STATE_ALARM_DISARMED
elif self._device.mode == "away":
elif self._device.is_away:
state = STATE_ALARM_ARMED_AWAY
elif self._device.mode == "home":
elif self._device.is_home:
state = STATE_ALARM_ARMED_HOME
else:
state = None
@@ -65,18 +56,21 @@ class AbodeAlarm(alarm.AlarmControlPanel):
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._device.set_standby()
self.schedule_update_ha_state()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._device.set_home()
self.schedule_update_ha_state()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._device.set_away()
self.schedule_update_ha_state()
def update(self):
"""Update the device state."""
self._device.refresh()
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'device_id': self._device.device_id,
'battery_backup': self._device.battery,
'cellular_backup': self._device.is_cellular
}
@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
REQUIREMENTS = ['pythonegardia==1.0.18']
REQUIREMENTS = ['pythonegardia==1.0.20']
_LOGGER = logging.getLogger(__name__)
@@ -29,7 +29,7 @@ CONF_REPORT_SERVER_PORT = 'report_server_port'
DEFAULT_NAME = 'Egardia'
DEFAULT_PORT = 80
DEFAULT_REPORT_SERVER_ENABLED = False
DEFAULT_REPORT_SERVER_PORT = 85
DEFAULT_REPORT_SERVER_PORT = 52010
DOMAIN = 'egardia'
NOTIFICATION_ID = 'egardia_notification'
@@ -154,8 +154,9 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def update(self):
"""Update the alarm status."""
status = self._egardiasystem.getstate()
self.parsestatus(status)
if not self._rs_enabled:
status = self._egardiasystem.getstate()
self.parsestatus(status)
def alarm_disarm(self, code=None):
"""Send disarm command."""
@@ -24,6 +24,8 @@ DEFAULT_PENDING_TIME = 60
DEFAULT_TRIGGER_TIME = 120
DEFAULT_DISARM_AFTER_TRIGGER = False
ATTR_POST_PENDING_STATE = 'post_pending_state'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'manual',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
@@ -101,7 +103,9 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._trigger_time) < dt_util.utcnow():
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
return self._pre_trigger_state
else:
self._state = self._pre_trigger_state
return self._state
return self._state
@@ -183,3 +187,13 @@ class ManualAlarm(alarm.AlarmControlPanel):
if not check:
_LOGGER.warning("Invalid code given for %s", state)
return check
@property
def device_state_attributes(self):
"""Return the state attributes."""
state_attr = {}
if self.state == STATE_ALARM_PENDING:
state_attr[ATTR_POST_PENDING_STATE] = self._state
return state_attr
@@ -12,16 +12,18 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
CONF_BELOW, CONF_ABOVE)
from homeassistant.helpers.event import async_track_state_change
CONF_BELOW, CONF_ABOVE, CONF_FOR)
from homeassistant.helpers.event import (
async_track_state_change, async_track_same_state)
from homeassistant.helpers import condition, config_validation as cv
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'numeric_state',
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
CONF_BELOW: vol.Coerce(float),
CONF_ABOVE: vol.Coerce(float),
vol.Optional(CONF_BELOW): vol.Coerce(float),
vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
_LOGGER = logging.getLogger(__name__)
@@ -33,15 +35,18 @@ def async_trigger(hass, config, action):
entity_id = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
time_delta = config.get(CONF_FOR)
value_template = config.get(CONF_VALUE_TEMPLATE)
async_remove_track_same = None
if value_template is not None:
value_template.hass = hass
@callback
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
def check_numeric_state(entity, from_s, to_s):
"""Return True if they should trigger."""
if to_s is None:
return
return False
variables = {
'trigger': {
@@ -55,17 +60,56 @@ def async_trigger(hass, config, action):
# If new one doesn't match, nothing to do
if not condition.async_numeric_state(
hass, to_s, below, above, value_template, variables):
return False
return True
@callback
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
nonlocal async_remove_track_same
if not check_numeric_state(entity, from_s, to_s):
return
variables = {
'trigger': {
'platform': 'numeric_state',
'entity_id': entity,
'below': below,
'above': above,
'from_state': from_s,
'to_state': to_s,
}
}
# Only match if old didn't exist or existed but didn't match
# Written as: skip if old one did exist and matched
if from_s is not None and condition.async_numeric_state(
hass, from_s, below, above, value_template, variables):
return
variables['trigger']['from_state'] = from_s
variables['trigger']['to_state'] = to_s
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, variables)
hass.async_run_job(action, variables)
if not time_delta:
call_action()
return
return async_track_state_change(hass, entity_id, state_automation_listener)
async_remove_track_same = async_track_same_state(
hass, True, time_delta, call_action, entity_ids=entity_id,
async_check_func=check_numeric_state)
unsub = async_track_state_change(
hass, entity_id, state_automation_listener)
@callback
def async_remove():
"""Remove state listeners async."""
unsub()
if async_remove_track_same:
async_remove_track_same() # pylint: disable=not-callable
return async_remove
+18 -56
View File
@@ -8,28 +8,23 @@ import asyncio
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.util.dt as dt_util
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
from homeassistant.helpers.event import (
async_track_state_change, async_track_point_in_utc_time)
async_track_state_change, async_track_same_state)
import homeassistant.helpers.config_validation as cv
CONF_ENTITY_ID = 'entity_id'
CONF_FROM = 'from'
CONF_TO = 'to'
CONF_FOR = 'for'
TRIGGER_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): 'state',
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
# These are str on purpose. Want to catch YAML conversions
CONF_FROM: str,
CONF_TO: str,
CONF_FOR: vol.All(cv.time_period, cv.positive_timedelta),
}),
cv.key_dependency(CONF_FOR, CONF_TO),
)
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'state',
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
# These are str on purpose. Want to catch YAML conversions
vol.Optional(CONF_FROM): str,
vol.Optional(CONF_TO): str,
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
}), cv.key_dependency(CONF_FOR, CONF_TO))
@asyncio.coroutine
@@ -39,28 +34,15 @@ def async_trigger(hass, config, action):
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO, MATCH_ALL)
time_delta = config.get(CONF_FOR)
async_remove_state_for_cancel = None
async_remove_state_for_listener = None
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
@callback
def clear_listener():
"""Clear all unsub listener."""
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
# pylint: disable=not-callable
if async_remove_state_for_listener is not None:
async_remove_state_for_listener()
async_remove_state_for_listener = None
if async_remove_state_for_cancel is not None:
async_remove_state_for_cancel()
async_remove_state_for_cancel = None
async_remove_track_same = None
@callback
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
nonlocal async_remove_track_same
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, {
@@ -78,33 +60,12 @@ def async_trigger(hass, config, action):
from_s.last_changed == to_s.last_changed):
return
if time_delta is None:
if not time_delta:
call_action()
return
@callback
def state_for_listener(now):
"""Fire on state changes after a delay and calls action."""
nonlocal async_remove_state_for_listener
async_remove_state_for_listener = None
clear_listener()
call_action()
@callback
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
"""Fire on changes and cancel for listener if changed."""
if inner_to_s.state == to_s.state:
return
clear_listener()
# cleanup previous listener
clear_listener()
async_remove_state_for_listener = async_track_point_in_utc_time(
hass, state_for_listener, dt_util.utcnow() + time_delta)
async_remove_state_for_cancel = async_track_state_change(
hass, entity, state_for_cancel_listener)
async_remove_track_same = async_track_same_state(
hass, to_s.state, time_delta, call_action, entity_ids=entity_id)
unsub = async_track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state)
@@ -113,6 +74,7 @@ def async_trigger(hass, config, action):
def async_remove():
"""Remove state listeners async."""
unsub()
clear_listener()
if async_remove_track_same:
async_remove_track_same() # pylint: disable=not-callable
return async_remove
+28 -48
View File
@@ -6,76 +6,56 @@ https://home-assistant.io/components/binary_sensor.abode/
"""
import logging
from homeassistant.components.abode import (CONF_ATTRIBUTION, DATA_ABODE)
from homeassistant.const import (ATTR_ATTRIBUTION)
from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.abode import AbodeDevice, DATA_ABODE
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['abode']
_LOGGER = logging.getLogger(__name__)
# Sensor types: Name, device_class
SENSOR_TYPES = {
'Door Contact': 'opening',
'Motion Camera': 'motion',
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for an Abode device."""
data = hass.data.get(DATA_ABODE)
abode = hass.data[DATA_ABODE]
device_types = map_abode_device_class().keys()
sensors = []
for sensor in data.devices:
_LOGGER.debug('Sensor type %s', sensor.type)
if sensor.type in ['Door Contact', 'Motion Camera']:
sensors.append(AbodeBinarySensor(hass, data, sensor))
for sensor in abode.get_devices(type_filter=device_types):
sensors.append(AbodeBinarySensor(abode, sensor))
_LOGGER.debug('Adding %d sensors', len(sensors))
add_devices(sensors)
class AbodeBinarySensor(BinarySensorDevice):
def map_abode_device_class():
"""Map Abode device types to Home Assistant binary sensor class."""
import abodepy.helpers.constants as CONST
return {
CONST.DEVICE_GLASS_BREAK: 'connectivity',
CONST.DEVICE_KEYPAD: 'connectivity',
CONST.DEVICE_DOOR_CONTACT: 'opening',
CONST.DEVICE_STATUS_DISPLAY: 'connectivity',
CONST.DEVICE_MOTION_CAMERA: 'connectivity',
CONST.DEVICE_WATER_SENSOR: 'moisture'
}
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
"""A binary sensor implementation for Abode device."""
def __init__(self, hass, data, device):
def __init__(self, controller, device):
"""Initialize a sensor for Abode device."""
super(AbodeBinarySensor, self).__init__()
self._device = device
@property
def should_poll(self):
"""Return the polling state."""
return True
@property
def name(self):
"""Return the name of the sensor."""
return "{0} {1}".format(self._device.type, self._device.name)
AbodeDevice.__init__(self, controller, device)
self._device_class = map_abode_device_class().get(self._device.type)
@property
def is_on(self):
"""Return True if the binary sensor is on."""
if self._device.type == 'Door Contact':
return self._device.status != 'Closed'
elif self._device.type == 'Motion Camera':
return self._device.get_value('motion_event') == '1'
return self._device.is_on
@property
def device_class(self):
"""Return the class of the binary sensor."""
return SENSOR_TYPES.get(self._device.type)
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
attrs['device_id'] = self._device.device_id
attrs['battery_low'] = self._device.battery_low
return attrs
def update(self):
"""Update the device state."""
self._device.refresh()
return self._device_class
@@ -0,0 +1,211 @@
"""
Use Bayesian Inference to trigger a binary sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.bayesian/
"""
import asyncio
import logging
from collections import OrderedDict
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
CONF_PLATFORM, CONF_STATE, STATE_UNKNOWN)
from homeassistant.core import callback
from homeassistant.helpers import condition
from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__)
CONF_OBSERVATIONS = 'observations'
CONF_PRIOR = 'prior'
CONF_PROBABILITY_THRESHOLD = 'probability_threshold'
CONF_P_GIVEN_F = 'prob_given_false'
CONF_P_GIVEN_T = 'prob_given_true'
CONF_TO_STATE = 'to_state'
DEFAULT_NAME = 'BayesianBinary'
NUMERIC_STATE_SCHEMA = vol.Schema({
CONF_PLATFORM: 'numeric_state',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ABOVE): vol.Coerce(float),
vol.Optional(CONF_BELOW): vol.Coerce(float),
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
}, required=True)
STATE_SCHEMA = vol.Schema({
CONF_PLATFORM: CONF_STATE,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TO_STATE): cv.string,
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
}, required=True)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME):
cv.string,
vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Required(CONF_OBSERVATIONS): vol.Schema(
vol.All(cv.ensure_list, [vol.Any(NUMERIC_STATE_SCHEMA,
STATE_SCHEMA)])
),
vol.Required(CONF_PRIOR): vol.Coerce(float),
vol.Optional(CONF_PROBABILITY_THRESHOLD):
vol.Coerce(float),
})
def update_probability(prior, prob_true, prob_false):
"""Update probability using Bayes' rule."""
numerator = prob_true * prior
denominator = numerator + prob_false * (1 - prior)
probability = numerator / denominator
return probability
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Threshold sensor."""
name = config.get(CONF_NAME)
observations = config.get(CONF_OBSERVATIONS)
prior = config.get(CONF_PRIOR)
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD, 0.5)
device_class = config.get(CONF_DEVICE_CLASS)
async_add_devices([
BayesianBinarySensor(name, prior, observations, probability_threshold,
device_class)
], True)
class BayesianBinarySensor(BinarySensorDevice):
"""Representation of a Bayesian sensor."""
def __init__(self, name, prior, observations, probability_threshold,
device_class):
"""Initialize the Bayesian sensor."""
self._name = name
self._observations = observations
self._probability_threshold = probability_threshold
self._device_class = device_class
self._deviation = False
self.prior = prior
self.probability = prior
self.current_obs = OrderedDict({})
self.entity_obs = {obs['entity_id']: obs for obs in self._observations}
self.watchers = {
'numeric_state': self._process_numeric_state,
'state': self._process_state
}
@asyncio.coroutine
def async_added_to_hass(self):
"""Call when entity about to be added to hass."""
@callback
# pylint: disable=invalid-name
def async_threshold_sensor_state_listener(entity, old_state,
new_state):
"""Handle sensor state changes."""
if new_state.state == STATE_UNKNOWN:
return
entity_obs = self.entity_obs[entity]
platform = entity_obs['platform']
self.watchers[platform](entity_obs)
prior = self.prior
print(self.current_obs.values())
for obs in self.current_obs.values():
prior = update_probability(prior, obs['prob_true'],
obs['prob_false'])
self.probability = prior
self.hass.async_add_job(self.async_update_ha_state, True)
entities = [obs['entity_id'] for obs in self._observations]
async_track_state_change(
self.hass, entities, async_threshold_sensor_state_listener)
def _update_current_obs(self, entity_observation, should_trigger):
"""Update current observation."""
entity = entity_observation['entity_id']
if should_trigger:
prob_true = entity_observation['prob_given_true']
prob_false = entity_observation.get(
'prob_given_false', 1 - prob_true)
self.current_obs[entity] = {
'prob_true': prob_true,
'prob_false': prob_false
}
else:
self.current_obs.pop(entity, None)
def _process_numeric_state(self, entity_observation):
"""Add entity to current_obs if numeric state conditions are met."""
entity = entity_observation['entity_id']
should_trigger = condition.async_numeric_state(
self.hass, entity,
entity_observation.get('below'),
entity_observation.get('above'), None, entity_observation)
self._update_current_obs(entity_observation, should_trigger)
def _process_state(self, entity_observation):
"""Add entity to current observations if state conditions are met."""
entity = entity_observation['entity_id']
should_trigger = condition.state(
self.hass, entity, entity_observation.get('to_state'))
self._update_current_obs(entity_observation, should_trigger)
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return true if sensor is on."""
return self._deviation
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_class(self):
"""Return the sensor class of the sensor."""
return self._device_class
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
'observations': [val for val in self.current_obs.values()],
'probability': self.probability,
'probability_threshold': self._probability_threshold
}
@asyncio.coroutine
def async_update(self):
"""Get the latest data and update the states."""
self._deviation = bool(self.probability > self._probability_threshold)
@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
REQUIREMENTS = ['pyhik==0.1.3']
REQUIREMENTS = ['pyhik==0.1.4']
_LOGGER = logging.getLogger(__name__)
CONF_IGNORED = 'ignored'
@@ -47,6 +47,7 @@ DEVICE_CLASS_MAP = {
'PIR Alarm': 'motion',
'Face Detection': 'motion',
'Scene Change Detection': 'motion',
'I/O': None,
}
CUSTOMIZE_SCHEMA = vol.Schema({
@@ -35,8 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
new_device = HMBinarySensor(hass, conf)
new_device.link_homematic()
new_device = HMBinarySensor(conf)
devices.append(new_device)
add_devices(devices)
+133 -9
View File
@@ -1,21 +1,145 @@
"""
Contains functionality to use a KNX group address as a binary.
Support for KNX/IP binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.knx/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.knx import (KNXConfig, KNXGroupAddress)
import asyncio
import voluptuous as vol
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES, \
KNXAutomation
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, \
BinarySensorDevice
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
CONF_ADDRESS = 'address'
CONF_DEVICE_CLASS = 'device_class'
CONF_SIGNIFICANT_BIT = 'significant_bit'
CONF_DEFAULT_SIGNIFICANT_BIT = 1
CONF_AUTOMATION = 'automation'
CONF_HOOK = 'hook'
CONF_DEFAULT_HOOK = 'on'
CONF_COUNTER = 'counter'
CONF_DEFAULT_COUNTER = 1
CONF_ACTION = 'action'
CONF__ACTION = 'turn_off_action'
DEFAULT_NAME = 'KNX Binary Sensor'
DEPENDENCIES = ['knx']
AUTOMATION_SCHEMA = vol.Schema({
vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string,
vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port,
vol.Required(CONF_ACTION, default=None): cv.SCRIPT_SCHEMA
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the KNX binary sensor platform."""
add_devices([KNXSwitch(hass, KNXConfig(config))])
AUTOMATIONS_SCHEMA = vol.All(
cv.ensure_list,
[AUTOMATION_SCHEMA]
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT):
cv.positive_int,
vol.Optional(CONF_AUTOMATION, default=None): AUTOMATIONS_SCHEMA,
})
class KNXSwitch(KNXGroupAddress, BinarySensorDevice):
"""Representation of a KNX binary sensor device."""
@asyncio.coroutine
def async_setup_platform(hass, config, add_devices,
discovery_info=None):
"""Set up binary sensor(s) for KNX platform."""
if DATA_KNX not in hass.data \
or not hass.data[DATA_KNX].initialized:
return False
pass
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, add_devices)
else:
async_add_devices_config(hass, config, add_devices)
return True
@callback
def async_add_devices_discovery(hass, discovery_info, add_devices):
"""Set up binary sensors for KNX platform configured via xknx.yaml."""
entities = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name]
entities.append(KNXBinarySensor(hass, device))
add_devices(entities)
@callback
def async_add_devices_config(hass, config, add_devices):
"""Set up binary senor for KNX platform configured within plattform."""
name = config.get(CONF_NAME)
import xknx
binary_sensor = xknx.devices.BinarySensor(
hass.data[DATA_KNX].xknx,
name=name,
group_address=config.get(CONF_ADDRESS),
device_class=config.get(CONF_DEVICE_CLASS),
significant_bit=config.get(CONF_SIGNIFICANT_BIT))
hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
entity = KNXBinarySensor(hass, binary_sensor)
automations = config.get(CONF_AUTOMATION)
if automations is not None:
for automation in automations:
counter = automation.get(CONF_COUNTER)
hook = automation.get(CONF_HOOK)
action = automation.get(CONF_ACTION)
entity.automations.append(KNXAutomation(
hass=hass, device=binary_sensor, hook=hook,
action=action, counter=counter))
add_devices([entity])
class KNXBinarySensor(BinarySensorDevice):
"""Representation of a KNX binary sensor."""
def __init__(self, hass, device):
"""Initialization of KNXBinarySensor."""
self.device = device
self.hass = hass
self.async_register_callbacks()
self.automations = []
@callback
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@asyncio.coroutine
def after_update_callback(device):
"""Callback after device was updated."""
# pylint: disable=unused-argument
yield from self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
@property
def name(self):
"""Return the name of the KNX device."""
return self.device.name
@property
def should_poll(self):
"""No polling needed within KNX."""
return False
@property
def device_class(self):
"""Return the class of this sensor."""
return self.device.device_class
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.device.is_on()
@@ -103,7 +103,8 @@ class RingBinarySensor(BinarySensorDevice):
self._data.check_alerts()
if self._data.alert:
self._state = (self._sensor_type ==
self._data.alert.get('kind'))
if self._sensor_type == self._data.alert.get('kind') and \
self._data.account_id == self._data.alert.get('doorbot_id'):
self._state = True
else:
self._state = False
@@ -19,16 +19,24 @@ from homeassistant.const import (
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.event import (
async_track_state_change, async_track_same_state)
from homeassistant.helpers.restore_state import async_get_last_state
_LOGGER = logging.getLogger(__name__)
CONF_DELAY_ON = 'delay_on'
CONF_DELAY_OFF = 'delay_off'
SENSOR_SCHEMA = vol.Schema({
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DELAY_ON):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_DELAY_OFF):
vol.All(cv.time_period, cv.positive_timedelta),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -47,6 +55,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
value_template.extract_entities())
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
device_class = device_config.get(CONF_DEVICE_CLASS)
delay_on = device_config.get(CONF_DELAY_ON)
delay_off = device_config.get(CONF_DELAY_OFF)
if value_template is not None:
value_template.hass = hass
@@ -54,13 +64,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
sensors.append(
BinarySensorTemplate(
hass, device, friendly_name, device_class, value_template,
entity_ids)
entity_ids, delay_on, delay_off)
)
if not sensors:
_LOGGER.error("No sensors added")
return False
async_add_devices(sensors, True)
async_add_devices(sensors)
return True
@@ -68,7 +78,7 @@ class BinarySensorTemplate(BinarySensorDevice):
"""A virtual binary sensor that triggers from another sensor."""
def __init__(self, hass, device, friendly_name, device_class,
value_template, entity_ids):
value_template, entity_ids, delay_on, delay_off):
"""Initialize the Template binary sensor."""
self.hass = hass
self.entity_id = async_generate_entity_id(
@@ -78,6 +88,8 @@ class BinarySensorTemplate(BinarySensorDevice):
self._template = value_template
self._state = None
self._entities = entity_ids
self._delay_on = delay_on
self._delay_off = delay_off
@asyncio.coroutine
def async_added_to_hass(self):
@@ -89,7 +101,7 @@ class BinarySensorTemplate(BinarySensorDevice):
@callback
def template_bsensor_state_listener(entity, old_state, new_state):
"""Handle the target device state changes."""
self.hass.async_add_job(self.async_update_ha_state(True))
self.async_check_state()
@callback
def template_bsensor_startup(event):
@@ -97,7 +109,7 @@ class BinarySensorTemplate(BinarySensorDevice):
async_track_state_change(
self.hass, self._entities, template_bsensor_state_listener)
self.hass.async_add_job(self.async_update_ha_state(True))
self.hass.async_add_job(self.async_check_state)
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
@@ -122,11 +134,11 @@ class BinarySensorTemplate(BinarySensorDevice):
"""No polling needed."""
return False
@asyncio.coroutine
def async_update(self):
"""Update the state from the template."""
@callback
def _async_render(self, *args):
"""Get the state of template."""
try:
self._state = self._template.async_render().lower() == 'true'
return self._template.async_render().lower() == 'true'
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
@@ -135,4 +147,29 @@ class BinarySensorTemplate(BinarySensorDevice):
"the state is unknown", self._name)
return
_LOGGER.error("Could not render template %s: %s", self._name, ex)
self._state = False
@callback
def async_check_state(self):
"""Update the state from the template."""
state = self._async_render()
# return if the state don't change or is invalid
if state is None or state == self.state:
return
@callback
def set_state():
"""Set state of template binary sensor."""
self._state = state
self.hass.async_add_job(self.async_update_ha_state())
# state without delay
if (state and not self._delay_on) or \
(not state and not self._delay_off):
set_state()
return
period = self._delay_on if state else self._delay_off
async_track_same_state(
self.hass, state, period, set_state, entity_ids=self._entities,
async_check_func=self._async_render)
@@ -0,0 +1,57 @@
"""
Support for Tesla binary sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.tesla/
"""
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['tesla']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tesla binary sensor."""
devices = [
TeslaBinarySensor(
device, hass.data[TESLA_DOMAIN]['controller'], 'connectivity')
for device in hass.data[TESLA_DOMAIN]['devices']['binary_sensor']]
add_devices(devices, True)
class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
"""Implement an Tesla binary sensor for parking and charger."""
def __init__(self, tesla_device, controller, sensor_type):
"""Initialisation of binary sensor."""
super().__init__(tesla_device, controller)
self._name = self.tesla_device.name
self._state = False
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
self._sensor_type = sensor_type
@property
def device_class(self):
"""Return the class of this binary sensor."""
return self._sensor_type
@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
def update(self):
"""Update the state of the device."""
_LOGGER.debug("Updating sensor: %s", self._name)
self.tesla_device.update()
self._state = self.tesla_device.get_value()
@@ -31,6 +31,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices.append(XiaomiDoorSensor(device, gateway))
elif model == 'sensor_magnet.aq2':
devices.append(XiaomiDoorSensor(device, gateway))
elif model == 'sensor_wleak.aq1':
devices.append(XiaomiWaterLeakSensor(device, gateway))
elif model == 'smoke':
devices.append(XiaomiSmokeSensor(device, gateway))
elif model == 'natgas':
@@ -214,6 +216,35 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
return False
class XiaomiWaterLeakSensor(XiaomiBinarySensor):
"""Representation of a XiaomiWaterLeakSensor."""
def __init__(self, device, xiaomi_hub):
"""Initialize the XiaomiWaterLeakSensor."""
XiaomiBinarySensor.__init__(self, device, 'Water Leak Sensor',
xiaomi_hub, 'status', 'moisture')
def parse_data(self, data):
"""Parse data sent by gateway."""
self._should_poll = False
value = data.get(self._data_key)
if value is None:
return False
if value == 'leak':
self._should_poll = True
if self._state:
return False
self._state = True
return True
elif value == 'no_leak':
if self._state:
self._state = False
return True
return False
class XiaomiSmokeSensor(XiaomiBinarySensor):
"""Representation of a XiaomiSmokeSensor."""
+2 -2
View File
@@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyfoscam==1.2']
REQUIREMENTS = ['libpyfoscam==1.0']
CONF_IP = 'ip'
@@ -53,7 +53,7 @@ class FoscamCam(Camera):
self._name = device_info.get(CONF_NAME)
self._motion_status = False
from foscam import FoscamCamera
from libpyfoscam import FoscamCamera
self._foscam_session = FoscamCamera(ip_address, port, self._username,
self._password, verbose=False)
@@ -47,8 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
new_device = HMThermostat(hass, conf)
new_device.link_homematic()
new_device = HMThermostat(conf)
devices.append(new_device)
add_devices(devices)
@@ -196,6 +196,11 @@ class RoundThermostat(ClimateDevice):
if val['id'] == self._id:
data = val
except KeyError:
_LOGGER.error("Update failed from Honeywell server")
self.client.user_data = None
return
except StopIteration:
_LOGGER.error("Did not receive any temperature data from the "
"evohomeclient API")
+130 -52
View File
@@ -1,68 +1,136 @@
"""
Support for KNX thermostats.
Support for KNX/IP climate devices.
For more details about this platform, please refer to the documentation
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.knx/
"""
import logging
import asyncio
import voluptuous as vol
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
from homeassistant.const import (CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE)
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_ADDRESS = 'address'
CONF_SETPOINT_ADDRESS = 'setpoint_address'
CONF_TEMPERATURE_ADDRESS = 'temperature_address'
CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address'
CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
CONF_OPERATION_MODE_STATE_ADDRESS = 'operation_mode_state_address'
CONF_CONTROLLER_STATUS_ADDRESS = 'controller_status_address'
CONF_CONTROLLER_STATUS_STATE_ADDRESS = 'controller_status_state_address'
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = \
'operation_mode_frost_protection_address'
CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address'
CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address'
DEFAULT_NAME = 'KNX Thermostat'
DEFAULT_NAME = 'KNX Climate'
DEPENDENCIES = ['knx']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_SETPOINT_ADDRESS): cv.string,
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Create and add an entity based on the configuration."""
add_devices([KNXThermostat(hass, KNXConfig(config))])
@asyncio.coroutine
def async_setup_platform(hass, config, add_devices,
discovery_info=None):
"""Set up climate(s) for KNX platform."""
if DATA_KNX not in hass.data \
or not hass.data[DATA_KNX].initialized:
return False
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, add_devices)
else:
async_add_devices_config(hass, config, add_devices)
return True
class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
"""Representation of a KNX thermostat.
@callback
def async_add_devices_discovery(hass, discovery_info, add_devices):
"""Set up climates for KNX platform configured within plattform."""
entities = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name]
entities.append(KNXClimate(hass, device))
add_devices(entities)
A KNX thermostat will has the following parameters:
- temperature (current temperature)
- setpoint (target temperature in HASS terms)
- operation mode selection (comfort/night/frost protection)
This version supports only polling. Messages from the KNX bus do not
automatically update the state of the thermostat (to be implemented
in future releases)
"""
@callback
def async_add_devices_config(hass, config, add_devices):
"""Set up climate for KNX platform configured within plattform."""
import xknx
climate = xknx.devices.Climate(
hass.data[DATA_KNX].xknx,
name=config.get(CONF_NAME),
group_address_temperature=config.get(
CONF_TEMPERATURE_ADDRESS),
group_address_target_temperature=config.get(
CONF_TARGET_TEMPERATURE_ADDRESS),
group_address_setpoint=config.get(
CONF_SETPOINT_ADDRESS),
group_address_operation_mode=config.get(
CONF_OPERATION_MODE_ADDRESS),
group_address_operation_mode_state=config.get(
CONF_OPERATION_MODE_STATE_ADDRESS),
group_address_controller_status=config.get(
CONF_CONTROLLER_STATUS_ADDRESS),
group_address_controller_status_state=config.get(
CONF_CONTROLLER_STATUS_STATE_ADDRESS),
group_address_operation_mode_protection=config.get(
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS),
group_address_operation_mode_night=config.get(
CONF_OPERATION_MODE_NIGHT_ADDRESS),
group_address_operation_mode_comfort=config.get(
CONF_OPERATION_MODE_COMFORT_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(climate)
add_devices([KNXClimate(hass, climate)])
def __init__(self, hass, config):
"""Initialize the thermostat based on the given configuration."""
KNXMultiAddressDevice.__init__(
self, hass, config, ['temperature', 'setpoint'], ['mode'])
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
class KNXClimate(ClimateDevice):
"""Representation of a KNX climate."""
def __init__(self, hass, device):
"""Initialization of KNXClimate."""
self.device = device
self.hass = hass
self.async_register_callbacks()
self._unit_of_measurement = TEMP_CELSIUS
self._away = False # not yet supported
self._is_fan_on = False # not yet supported
self._current_temp = None
self._target_temp = None
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@asyncio.coroutine
def after_update_callback(device):
"""Callback after device was updated."""
# pylint: disable=unused-argument
yield from self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
@property
def name(self):
"""Return the name of the KNX device."""
return self.device.name
@property
def should_poll(self):
"""Return the polling state, is needed for the KNX thermostat."""
return True
"""No polling needed within KNX."""
return False
@property
def temperature_unit(self):
@@ -72,32 +140,42 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temp
return self.device.temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temp
if self.device.supports_target_temperature:
return self.device.target_temperature
return None
def set_temperature(self, **kwargs):
@asyncio.coroutine
def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
from knxip.conversion import float_to_knx2
if self.device.supports_target_temperature:
yield from self.device.set_target_temperature(temperature)
self.set_value('setpoint', float_to_knx2(temperature))
_LOGGER.debug("Set target temperature to %s", temperature)
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.device.supports_operation_mode:
return self.device.operation_mode.value
return None
def set_operation_mode(self, operation_mode):
@property
def operation_list(self):
"""Return the list of available operation modes."""
return [operation_mode.value for
operation_mode in
self.device.get_supported_operation_modes()]
@asyncio.coroutine
def async_set_operation_mode(self, operation_mode):
"""Set operation mode."""
raise NotImplementedError()
def update(self):
"""Update KNX climate."""
from knxip.conversion import knx2_to_float
super().update()
self._current_temp = knx2_to_float(self.value('temperature'))
self._target_temp = knx2_to_float(self.value('setpoint'))
if self.device.supports_operation_mode:
from xknx.knx import HVACOperationMode
knx_operation_mode = HVACOperationMode(operation_mode)
yield from self.device.set_operation_mode(knx_operation_mode)
+93
View File
@@ -0,0 +1,93 @@
"""
Support for Tesla HVAC system.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/climate.tesla/
"""
import logging
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['tesla']
OPERATION_LIST = [STATE_ON, STATE_OFF]
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tesla climate platform."""
devices = [TeslaThermostat(device, hass.data[TESLA_DOMAIN]['controller'])
for device in hass.data[TESLA_DOMAIN]['devices']['climate']]
add_devices(devices, True)
class TeslaThermostat(TeslaDevice, ClimateDevice):
"""Representation of a Tesla climate."""
def __init__(self, tesla_device, controller):
"""Initialize the Tesla device."""
super().__init__(tesla_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
self._target_temperature = None
self._temperature = None
self._name = self.tesla_device.name
@property
def current_operation(self):
"""Return current operation ie. On or Off."""
mode = self.tesla_device.is_hvac_enabled()
if mode:
return OPERATION_LIST[0] # On
else:
return OPERATION_LIST[1] # Off
@property
def operation_list(self):
"""List of available operation modes."""
return OPERATION_LIST
def update(self):
"""Called by the Tesla device callback to update state."""
_LOGGER.debug("Updating: %s", self._name)
self.tesla_device.update()
self._target_temperature = self.tesla_device.get_goal_temp()
self._temperature = self.tesla_device.get_current_temp()
@property
def temperature_unit(self):
"""Return the unit of measurement."""
tesla_temp_units = self.tesla_device.measurement
if tesla_temp_units == 'F':
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self._temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, **kwargs):
"""Set new target temperatures."""
_LOGGER.debug("Setting temperature for: %s", self._name)
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature:
self.tesla_device.set_temperature(temperature)
def set_operation_mode(self, operation_mode):
"""Set HVAC mode (auto, cool, heat, off)."""
_LOGGER.debug("Setting mode for: %s", self._name)
if operation_mode == OPERATION_LIST[1]: # off
self.tesla_device.set_status(False)
elif operation_mode == OPERATION_LIST[0]: # heat
self.tesla_device.set_status(True)
@@ -0,0 +1,49 @@
"""Component to integrate the Home Assistant cloud."""
import asyncio
import logging
import voluptuous as vol
from . import http_api, cloud_api
from .const import DOMAIN
DEPENDENCIES = ['http']
CONF_MODE = 'mode'
MODE_DEV = 'development'
MODE_STAGING = 'staging'
MODE_PRODUCTION = 'production'
DEFAULT_MODE = MODE_DEV
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV, MODE_STAGING, MODE_PRODUCTION]),
}),
}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup(hass, config):
"""Initialize the Home Assistant cloud."""
mode = MODE_PRODUCTION
if DOMAIN in config:
mode = config[DOMAIN].get(CONF_MODE)
if mode != 'development':
_LOGGER.error('Only development mode is currently allowed.')
return False
data = hass.data[DOMAIN] = {
'mode': mode
}
cloud = yield from cloud_api.async_load_auth(hass)
if cloud is not None:
data['cloud'] = cloud
yield from http_api.async_setup(hass)
return True
+297
View File
@@ -0,0 +1,297 @@
"""Package to offer tools to communicate with the cloud."""
import asyncio
from datetime import timedelta
import json
import logging
import os
from urllib.parse import urljoin
import aiohttp
import async_timeout
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.dt import utcnow
from .const import AUTH_FILE, REQUEST_TIMEOUT, SERVERS
from .util import get_mode
_LOGGER = logging.getLogger(__name__)
URL_CREATE_TOKEN = 'o/token/'
URL_REVOKE_TOKEN = 'o/revoke_token/'
URL_ACCOUNT = 'account.json'
class CloudError(Exception):
"""Base class for cloud related errors."""
def __init__(self, reason=None, status=None):
"""Initialize a cloud error."""
super().__init__(reason)
self.status = status
class Unauthenticated(CloudError):
"""Raised when authentication failed."""
class UnknownError(CloudError):
"""Raised when an unknown error occurred."""
@asyncio.coroutine
def async_load_auth(hass):
"""Load authentication from disk and verify it."""
auth = yield from hass.async_add_job(_read_auth, hass)
if not auth:
return None
cloud = Cloud(hass, auth)
try:
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
auth_check = yield from cloud.async_refresh_account_info()
if not auth_check:
_LOGGER.error('Unable to validate credentials.')
return None
return cloud
except asyncio.TimeoutError:
_LOGGER.error('Unable to reach server to validate credentials.')
return None
@asyncio.coroutine
def async_login(hass, username, password, scope=None):
"""Get a token using a username and password.
Returns a coroutine.
"""
data = {
'grant_type': 'password',
'username': username,
'password': password
}
if scope is not None:
data['scope'] = scope
auth = yield from _async_get_token(hass, data)
yield from hass.async_add_job(_write_auth, hass, auth)
return Cloud(hass, auth)
@asyncio.coroutine
def _async_get_token(hass, data):
"""Get a new token and return it as a dictionary.
Raises exceptions when errors occur:
- Unauthenticated
- UnknownError
"""
session = async_get_clientsession(hass)
auth = aiohttp.BasicAuth(*_client_credentials(hass))
try:
req = yield from session.post(
_url(hass, URL_CREATE_TOKEN),
data=data,
auth=auth
)
if req.status == 401:
_LOGGER.error('Cloud login failed: %d', req.status)
raise Unauthenticated(status=req.status)
elif req.status != 200:
_LOGGER.error('Cloud login failed: %d', req.status)
raise UnknownError(status=req.status)
response = yield from req.json()
response['expires_at'] = \
(utcnow() + timedelta(seconds=response['expires_in'])).isoformat()
return response
except aiohttp.ClientError:
raise UnknownError()
class Cloud:
"""Store Hass Cloud info."""
def __init__(self, hass, auth):
"""Initialize Hass cloud info object."""
self.hass = hass
self.auth = auth
self.account = None
@property
def access_token(self):
"""Return access token."""
return self.auth['access_token']
@property
def refresh_token(self):
"""Get refresh token."""
return self.auth['refresh_token']
@asyncio.coroutine
def async_refresh_account_info(self):
"""Refresh the account info."""
req = yield from self.async_request('get', URL_ACCOUNT)
if req.status != 200:
return False
self.account = yield from req.json()
return True
@asyncio.coroutine
def async_refresh_access_token(self):
"""Get a token using a refresh token."""
try:
self.auth = yield from _async_get_token(self.hass, {
'grant_type': 'refresh_token',
'refresh_token': self.refresh_token,
})
yield from self.hass.async_add_job(
_write_auth, self.hass, self.auth)
return True
except CloudError:
return False
@asyncio.coroutine
def async_revoke_access_token(self):
"""Revoke active access token."""
session = async_get_clientsession(self.hass)
client_id, client_secret = _client_credentials(self.hass)
data = {
'token': self.access_token,
'client_id': client_id,
'client_secret': client_secret
}
try:
req = yield from session.post(
_url(self.hass, URL_REVOKE_TOKEN),
data=data,
)
if req.status != 200:
_LOGGER.error('Cloud logout failed: %d', req.status)
raise UnknownError(status=req.status)
self.auth = None
yield from self.hass.async_add_job(
_write_auth, self.hass, None)
except aiohttp.ClientError:
raise UnknownError()
@asyncio.coroutine
def async_request(self, method, path, **kwargs):
"""Make a request to Home Assistant cloud.
Will refresh the token if necessary.
"""
session = async_get_clientsession(self.hass)
url = _url(self.hass, path)
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['authorization'] = \
'Bearer {}'.format(self.access_token)
request = yield from session.request(method, url, **kwargs)
if request.status != 403:
return request
# Maybe token expired. Try refreshing it.
reauth = yield from self.async_refresh_access_token()
if not reauth:
return request
# Release old connection back to the pool.
yield from request.release()
kwargs['headers']['authorization'] = \
'Bearer {}'.format(self.access_token)
# If we are not already fetching the account info,
# refresh the account info.
if path != URL_ACCOUNT:
yield from self.async_refresh_account_info()
request = yield from session.request(method, url, **kwargs)
return request
def _read_auth(hass):
"""Read auth file."""
path = hass.config.path(AUTH_FILE)
if not os.path.isfile(path):
return None
with open(path) as file:
return json.load(file).get(get_mode(hass))
def _write_auth(hass, data):
"""Write auth info for specified mode.
Pass in None for data to remove authentication for that mode.
"""
path = hass.config.path(AUTH_FILE)
mode = get_mode(hass)
if os.path.isfile(path):
with open(path) as file:
content = json.load(file)
else:
content = {}
if data is None:
content.pop(mode, None)
else:
content[mode] = data
with open(path, 'wt') as file:
file.write(json.dumps(content, indent=4, sort_keys=True))
def _client_credentials(hass):
"""Get the client credentials.
Async friendly.
"""
mode = get_mode(hass)
if mode not in SERVERS:
raise ValueError('Mode {} is not supported.'.format(mode))
return SERVERS[mode]['client_id'], SERVERS[mode]['client_secret']
def _url(hass, path):
"""Generate a url for the cloud.
Async friendly.
"""
mode = get_mode(hass)
if mode not in SERVERS:
raise ValueError('Mode {} is not supported.'.format(mode))
return urljoin(SERVERS[mode]['host'], path)
+14
View File
@@ -0,0 +1,14 @@
"""Constants for the cloud component."""
DOMAIN = 'cloud'
REQUEST_TIMEOUT = 10
AUTH_FILE = '.cloud'
SERVERS = {
'development': {
'host': 'http://localhost:8000',
'client_id': 'HBhQxeV8H4aFBcs7jrZUeeDud0FjGEJJSZ9G6gNu',
'client_secret': ('V1qw2NhB32cSAlP7DOezjgWNgn7ZKgq0jvVZoYSI0KCmg9rg7q4'
'BSzoebnQnX6tuHCJiZjm2479mZmmtf2LOUdnSqOqkSpjc3js7Wu'
'VBJrRyfgTVd43kbrEQtuOiaUpK')
}
}
+119
View File
@@ -0,0 +1,119 @@
"""The HTTP api to control the cloud integration."""
import asyncio
import logging
import voluptuous as vol
import async_timeout
from homeassistant.components.http import HomeAssistantView
from . import cloud_api
from .const import DOMAIN, REQUEST_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup(hass):
"""Initialize the HTTP api."""
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
hass.http.register_view(CloudAccountView)
class CloudLoginView(HomeAssistantView):
"""Login to Home Assistant cloud."""
url = '/api/cloud/login'
name = 'api:cloud:login'
schema = vol.Schema({
vol.Required('username'): str,
vol.Required('password'): str,
})
@asyncio.coroutine
def post(self, request):
"""Validate config and return results."""
try:
data = yield from request.json()
except ValueError:
_LOGGER.error('Login with invalid JSON')
return self.json_message('Invalid JSON.', 400)
try:
self.schema(data)
except vol.Invalid as err:
_LOGGER.error('Login with invalid formatted data')
return self.json_message(
'Message format incorrect: {}'.format(err), 400)
hass = request.app['hass']
phase = 1
try:
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
cloud = yield from cloud_api.async_login(
hass, data['username'], data['password'])
phase += 1
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from cloud.async_refresh_account_info()
except cloud_api.Unauthenticated:
return self.json_message(
'Authentication failed (phase {}).'.format(phase), 401)
except cloud_api.UnknownError:
return self.json_message(
'Unknown error occurred (phase {}).'.format(phase), 500)
except asyncio.TimeoutError:
return self.json_message(
'Unable to reach Home Assistant cloud '
'(phase {}).'.format(phase), 502)
hass.data[DOMAIN]['cloud'] = cloud
return self.json(cloud.account)
class CloudLogoutView(HomeAssistantView):
"""Log out of the Home Assistant cloud."""
url = '/api/cloud/logout'
name = 'api:cloud:logout'
@asyncio.coroutine
def post(self, request):
"""Validate config and return results."""
hass = request.app['hass']
try:
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from \
hass.data[DOMAIN]['cloud'].async_revoke_access_token()
hass.data[DOMAIN].pop('cloud')
return self.json({
'result': 'ok',
})
except asyncio.TimeoutError:
return self.json_message("Could not reach the server.", 502)
except cloud_api.UnknownError as err:
return self.json_message(
"Error communicating with the server ({}).".format(err.status),
502)
class CloudAccountView(HomeAssistantView):
"""Log out of the Home Assistant cloud."""
url = '/api/cloud/account'
name = 'api:cloud:account'
@asyncio.coroutine
def get(self, request):
"""Validate config and return results."""
hass = request.app['hass']
if 'cloud' not in hass.data[DOMAIN]:
return self.json_message('Not logged in', 400)
return self.json(hass.data[DOMAIN]['cloud'].account)
+10
View File
@@ -0,0 +1,10 @@
"""Utilities for the cloud integration."""
from .const import DOMAIN
def get_mode(hass):
"""Return the current mode of the cloud component.
Async friendly.
"""
return hass.data[DOMAIN]['mode']
+10 -10
View File
@@ -14,7 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'group', 'hassbian', 'automation', 'script')
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
ON_DEMAND = ('zwave')
@@ -77,11 +77,11 @@ class BaseEditConfigView(HomeAssistantView):
"""Empty config if file not found."""
raise NotImplementedError
def _get_value(self, data, config_key):
def _get_value(self, hass, data, config_key):
"""Get value."""
raise NotImplementedError
def _write_value(self, data, config_key, new_value):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
raise NotImplementedError
@@ -90,7 +90,7 @@ class BaseEditConfigView(HomeAssistantView):
"""Fetch device specific config."""
hass = request.app['hass']
current = yield from self.read_config(hass)
value = self._get_value(current, config_key)
value = self._get_value(hass, current, config_key)
if value is None:
return self.json_message('Resource not found', 404)
@@ -121,7 +121,7 @@ class BaseEditConfigView(HomeAssistantView):
path = hass.config.path(self.path)
current = yield from self.read_config(hass)
self._write_value(current, config_key, data)
self._write_value(hass, current, config_key, data)
yield from hass.async_add_job(_write, path, current)
@@ -149,11 +149,11 @@ class EditKeyBasedConfigView(BaseEditConfigView):
"""Return an empty config."""
return {}
def _get_value(self, data, config_key):
def _get_value(self, hass, data, config_key):
"""Get value."""
return data.get(config_key, {})
def _write_value(self, data, config_key, new_value):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
data.setdefault(config_key, {}).update(new_value)
@@ -165,14 +165,14 @@ class EditIdBasedConfigView(BaseEditConfigView):
"""Return an empty config."""
return []
def _get_value(self, data, config_key):
def _get_value(self, hass, data, config_key):
"""Get value."""
return next(
(val for val in data if val.get(CONF_ID) == config_key), None)
def _write_value(self, data, config_key, new_value):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
value = self._get_value(data, config_key)
value = self._get_value(hass, data, config_key)
if value is None:
value = {CONF_ID: config_key}
@@ -0,0 +1,39 @@
"""Provide configuration end points for Customize."""
import asyncio
from homeassistant.components.config import EditKeyBasedConfigView
from homeassistant.components import async_reload_core_config
from homeassistant.config import DATA_CUSTOMIZE
import homeassistant.helpers.config_validation as cv
CONFIG_PATH = 'customize.yaml'
@asyncio.coroutine
def async_setup(hass):
"""Set up the Customize config API."""
hass.http.register_view(CustomizeConfigView(
'customize', 'config', CONFIG_PATH, cv.entity_id, dict,
post_write_hook=async_reload_core_config
))
return True
class CustomizeConfigView(EditKeyBasedConfigView):
"""Configure a list of entries."""
def _get_value(self, hass, data, config_key):
"""Get value."""
customize = hass.data.get(DATA_CUSTOMIZE, {}).get(config_key) or {}
return {'global': customize, 'local': data.get(config_key, {})}
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
data[config_key] = new_value
state = hass.states.get(config_key)
state_attributes = dict(state.attributes)
state_attributes.update(new_value)
hass.states.async_set(config_key, state.state, state_attributes)
+220
View File
@@ -0,0 +1,220 @@
"""
Component to count within automations.
For more details about this component, please refer to the documentation
at https://home-assistant.io/components/counter/
"""
import asyncio
import logging
import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME)
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.loader import bind_hass
_LOGGER = logging.getLogger(__name__)
ATTR_INITIAL = 'initial'
ATTR_STEP = 'step'
CONF_INITIAL = 'initial'
CONF_STEP = 'step'
DEFAULT_INITIAL = 0
DEFAULT_STEP = 1
DOMAIN = 'counter'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_DECREMENT = 'decrement'
SERVICE_INCREMENT = 'increment'
SERVICE_RESET = 'reset'
SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: vol.Any({
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
cv.positive_int,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
}, None)
})
}, extra=vol.ALLOW_EXTRA)
@bind_hass
def increment(hass, entity_id):
"""Increment a counter."""
hass.add_job(async_increment, hass, entity_id)
@callback
@bind_hass
def async_increment(hass, entity_id):
"""Increment a counter."""
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id}))
@bind_hass
def decrement(hass, entity_id):
"""Decrement a counter."""
hass.add_job(async_decrement, hass, entity_id)
@callback
@bind_hass
def async_decrement(hass, entity_id):
"""Decrement a counter."""
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id}))
@bind_hass
def reset(hass, entity_id):
"""Reset a counter."""
hass.add_job(async_reset, hass, entity_id)
@callback
@bind_hass
def async_reset(hass, entity_id):
"""Reset a counter."""
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id}))
@asyncio.coroutine
def async_setup(hass, config):
"""Set up a counter."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
entities = []
for object_id, cfg in config[DOMAIN].items():
if not cfg:
cfg = {}
name = cfg.get(CONF_NAME)
initial = cfg.get(CONF_INITIAL)
step = cfg.get(CONF_STEP)
icon = cfg.get(CONF_ICON)
entities.append(Counter(object_id, name, initial, step, icon))
if not entities:
return False
@asyncio.coroutine
def async_handler_service(service):
"""Handle a call to the counter services."""
target_counters = component.async_extract_from_service(service)
if service.service == SERVICE_INCREMENT:
attr = 'async_increment'
elif service.service == SERVICE_DECREMENT:
attr = 'async_decrement'
elif service.service == SERVICE_RESET:
attr = 'async_reset'
tasks = [getattr(counter, attr)() for counter in target_counters]
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register(
DOMAIN, SERVICE_INCREMENT, async_handler_service,
descriptions[DOMAIN][SERVICE_INCREMENT], SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_DECREMENT, async_handler_service,
descriptions[DOMAIN][SERVICE_DECREMENT], SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RESET, async_handler_service,
descriptions[DOMAIN][SERVICE_RESET], SERVICE_SCHEMA)
yield from component.async_add_entities(entities)
return True
class Counter(Entity):
"""Representation of a counter."""
def __init__(self, object_id, name, initial, step, icon):
"""Initialize a counter."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
self._step = step
self._state = self._initial = initial
self._icon = icon
@property
def should_poll(self):
"""If entity should be polled."""
return False
@property
def name(self):
"""Return name of the counter."""
return self._name
@property
def icon(self):
"""Return the icon to be used for this entity."""
return self._icon
@property
def state(self):
"""Return the current value of the counter."""
return self._state
@property
def state_attributes(self):
"""Return the state attributes."""
return {
ATTR_INITIAL: self._initial,
ATTR_STEP: self._step,
}
@asyncio.coroutine
def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant."""
# If not None, we got an initial value.
if self._state is not None:
return
state = yield from async_get_last_state(self.hass, self.entity_id)
self._state = state and state.state == state
@asyncio.coroutine
def async_decrement(self):
"""Decrement the counter."""
self._state -= self._step
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_increment(self):
"""Increment a counter."""
self._state += self._step
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_reset(self):
"""Reset a counter."""
self._state = self._initial
yield from self.async_update_ha_state()
+49
View File
@@ -0,0 +1,49 @@
"""
This component provides HA cover support for Abode Security System.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.abode/
"""
import logging
from homeassistant.components.abode import AbodeDevice, DATA_ABODE
from homeassistant.components.cover import CoverDevice
DEPENDENCIES = ['abode']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Abode cover devices."""
import abodepy.helpers.constants as CONST
abode = hass.data[DATA_ABODE]
sensors = []
for sensor in abode.get_devices(type_filter=(CONST.DEVICE_SECURE_BARRIER)):
sensors.append(AbodeCover(abode, sensor))
add_devices(sensors)
class AbodeCover(AbodeDevice, CoverDevice):
"""Representation of an Abode cover."""
def __init__(self, controller, device):
"""Initialize the Abode device."""
AbodeDevice.__init__(self, controller, device)
@property
def is_closed(self):
"""Return true if cover is closed, else False."""
return self._device.is_open is False
def close_cover(self):
"""Issue close command to cover."""
self._device.close_cover()
def open_cover(self):
"""Issue open command to cover."""
self._device.open_cover()
+1 -2
View File
@@ -21,8 +21,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
new_device = HMCover(hass, conf)
new_device.link_homematic()
new_device = HMCover(conf)
devices.append(new_device)
add_devices(devices)
+187 -133
View File
@@ -1,185 +1,239 @@
"""
Support for KNX covers.
Support for KNX/IP covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.knx/
"""
import logging
import asyncio
import voluptuous as vol
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
from homeassistant.helpers.event import async_track_utc_time_change
from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, SUPPORT_STOP,
SUPPORT_SET_TILT_POSITION
)
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
from homeassistant.const import (CONF_NAME, CONF_DEVICE_CLASS)
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE,
SUPPORT_SET_POSITION, SUPPORT_STOP, SUPPORT_SET_TILT_POSITION,
ATTR_POSITION, ATTR_TILT_POSITION)
from homeassistant.core import callback
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_GETPOSITION_ADDRESS = 'getposition_address'
CONF_SETPOSITION_ADDRESS = 'setposition_address'
CONF_GETANGLE_ADDRESS = 'getangle_address'
CONF_SETANGLE_ADDRESS = 'setangle_address'
CONF_STOP = 'stop_address'
CONF_UPDOWN = 'updown_address'
CONF_MOVE_LONG_ADDRESS = 'move_long_address'
CONF_MOVE_SHORT_ADDRESS = 'move_short_address'
CONF_POSITION_ADDRESS = 'position_address'
CONF_POSITION_STATE_ADDRESS = 'position_state_address'
CONF_ANGLE_ADDRESS = 'angle_address'
CONF_ANGLE_STATE_ADDRESS = 'angle_state_address'
CONF_TRAVELLING_TIME_DOWN = 'travelling_time_down'
CONF_TRAVELLING_TIME_UP = 'travelling_time_up'
CONF_INVERT_POSITION = 'invert_position'
CONF_INVERT_ANGLE = 'invert_angle'
DEFAULT_TRAVEL_TIME = 25
DEFAULT_NAME = 'KNX Cover'
DEPENDENCIES = ['knx']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_UPDOWN): cv.string,
vol.Required(CONF_STOP): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_GETPOSITION_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SETPOSITION_ADDRESS): cv.string,
vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string,
vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string,
vol.Optional(CONF_POSITION_ADDRESS): cv.string,
vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string,
vol.Optional(CONF_ANGLE_ADDRESS): cv.string,
vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string,
vol.Optional(CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME):
cv.positive_int,
vol.Optional(CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME):
cv.positive_int,
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
vol.Inclusive(CONF_GETANGLE_ADDRESS, 'angle'): cv.string,
vol.Inclusive(CONF_SETANGLE_ADDRESS, 'angle'): cv.string,
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Create and add an entity based on the configuration."""
add_devices([KNXCover(hass, KNXConfig(config))])
@asyncio.coroutine
def async_setup_platform(hass, config, add_devices,
discovery_info=None):
"""Set up cover(s) for KNX platform."""
if DATA_KNX not in hass.data \
or not hass.data[DATA_KNX].initialized:
return False
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, add_devices)
else:
async_add_devices_config(hass, config, add_devices)
return True
class KNXCover(KNXMultiAddressDevice, CoverDevice):
"""Representation of a KNX cover. e.g. a rollershutter."""
@callback
def async_add_devices_discovery(hass, discovery_info, add_devices):
"""Set up covers for KNX platform configured via xknx.yaml."""
entities = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name]
entities.append(KNXCover(hass, device))
add_devices(entities)
def __init__(self, hass, config):
@callback
def async_add_devices_config(hass, config, add_devices):
"""Set up cover for KNX platform configured within plattform."""
import xknx
cover = xknx.devices.Cover(
hass.data[DATA_KNX].xknx,
name=config.get(CONF_NAME),
group_address_long=config.get(CONF_MOVE_LONG_ADDRESS),
group_address_short=config.get(CONF_MOVE_SHORT_ADDRESS),
group_address_position_state=config.get(
CONF_POSITION_STATE_ADDRESS),
group_address_angle=config.get(CONF_ANGLE_ADDRESS),
group_address_angle_state=config.get(CONF_ANGLE_STATE_ADDRESS),
group_address_position=config.get(CONF_POSITION_ADDRESS),
travel_time_down=config.get(CONF_TRAVELLING_TIME_DOWN),
travel_time_up=config.get(CONF_TRAVELLING_TIME_UP))
invert_position = config.get(CONF_INVERT_POSITION)
invert_angle = config.get(CONF_INVERT_ANGLE)
hass.data[DATA_KNX].xknx.devices.add(cover)
add_devices([KNXCover(hass, cover, invert_position, invert_angle)])
class KNXCover(CoverDevice):
"""Representation of a KNX cover."""
def __init__(self, hass, device, invert_position=False,
invert_angle=False):
"""Initialize the cover."""
KNXMultiAddressDevice.__init__(
self, hass, config,
['updown', 'stop'], # required
optional=['setposition', 'getposition',
'getangle', 'setangle']
)
self._device_class = config.config.get(CONF_DEVICE_CLASS)
self._invert_position = config.config.get(CONF_INVERT_POSITION)
self._invert_angle = config.config.get(CONF_INVERT_ANGLE)
self._hass = hass
self._current_pos = None
self._target_pos = None
self._current_tilt = None
self._target_tilt = None
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
SUPPORT_SET_POSITION | SUPPORT_STOP
self.device = device
self.invert_position = invert_position
self.invert_angle = invert_angle
self.hass = hass
self.async_register_callbacks()
# Tilt is only supported, if there is a angle get and set address
if CONF_SETANGLE_ADDRESS in config.config:
_LOGGER.debug("%s: Tilt supported at addresses %s, %s",
self.name, config.config.get(CONF_SETANGLE_ADDRESS),
config.config.get(CONF_GETANGLE_ADDRESS))
self._supported_features = self._supported_features | \
SUPPORT_SET_TILT_POSITION
self._unsubscribe_auto_updater = None
@callback
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@asyncio.coroutine
def after_update_callback(device):
"""Callback after device was updated."""
# pylint: disable=unused-argument
yield from self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
@property
def name(self):
"""Return the name of the KNX device."""
return self.device.name
@property
def should_poll(self):
"""Polling is needed for the KNX cover."""
return True
"""No polling needed within KNX."""
return False
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
SUPPORT_SET_POSITION | SUPPORT_STOP
if self.device.supports_angle:
supported_features |= SUPPORT_SET_TILT_POSITION
return supported_features
@property
def current_cover_position(self):
"""Return the current position of the cover."""
return int(self.from_knx_position(
self.device.current_position(),
self.invert_position))
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
return self.device.is_closed()
@property
def current_cover_position(self):
"""Return current position of cover.
@asyncio.coroutine
def async_close_cover(self, **kwargs):
"""Close the cover."""
if not self.device.is_closed():
yield from self.device.set_down()
self.start_auto_updater()
None is unknown, 0 is closed, 100 is fully open.
"""
return self._current_pos
@asyncio.coroutine
def async_open_cover(self, **kwargs):
"""Open the cover."""
if not self.device.is_open():
yield from self.device.set_up()
self.start_auto_updater()
@property
def target_position(self):
"""Return the position we are trying to reach: 0 - 100."""
return self._target_pos
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
knx_position = self.to_knx_position(position, self.invert_position)
yield from self.device.set_position(knx_position)
self.start_auto_updater()
@asyncio.coroutine
def async_stop_cover(self, **kwargs):
"""Stop the cover."""
yield from self.device.stop()
self.stop_auto_updater()
@property
def current_cover_tilt_position(self):
"""Return current position of cover.
"""Return current tilt position of cover."""
if not self.device.supports_angle:
return None
return int(self.from_knx_position(
self.device.angle,
self.invert_angle))
None is unknown, 0 is closed, 100 is fully open.
"""
return self._current_tilt
@asyncio.coroutine
def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
if ATTR_TILT_POSITION in kwargs:
position = kwargs[ATTR_TILT_POSITION]
knx_position = self.to_knx_position(position, self.invert_angle)
yield from self.device.set_angle(knx_position)
@property
def target_tilt(self):
"""Return the tilt angle (in %) we are trying to reach: 0 - 100."""
return self._target_tilt
def start_auto_updater(self):
"""Start the autoupdater to update HASS while cover is moving."""
if self._unsubscribe_auto_updater is None:
self._unsubscribe_auto_updater = async_track_utc_time_change(
self.hass, self.auto_updater_hook)
def set_cover_position(self, **kwargs):
"""Set new target position."""
position = kwargs.get(ATTR_POSITION)
if position is None:
return
def stop_auto_updater(self):
"""Stop the autoupdater."""
if self._unsubscribe_auto_updater is not None:
self._unsubscribe_auto_updater()
self._unsubscribe_auto_updater = None
if self._invert_position:
position = 100-position
@callback
def auto_updater_hook(self, now):
"""Callback for autoupdater."""
# pylint: disable=unused-argument
self.hass.async_add_job(self.async_update_ha_state())
if self.device.position_reached():
self.stop_auto_updater()
self._target_pos = position
self.set_percentage('setposition', position)
_LOGGER.debug("%s: Set target position to %d", self.name, position)
self.hass.add_job(self.device.auto_stop_if_necessary())
def update(self):
"""Update device state."""
super().update()
value = self.get_percentage('getposition')
if value is not None:
self._current_pos = value
if self._invert_position:
self._current_pos = 100-value
_LOGGER.debug("%s: position = %d", self.name, value)
@staticmethod
def from_knx_position(raw, invert):
"""Convert KNX position [0...255] to hass position [100...0]."""
position = round((raw/256)*100)
if not invert:
position = 100 - position
return position
if self._supported_features & SUPPORT_SET_TILT_POSITION:
value = self.get_percentage('getangle')
if value is not None:
self._current_tilt = value
if self._invert_angle:
self._current_tilt = 100-value
_LOGGER.debug("%s: tilt = %d", self.name, value)
def open_cover(self, **kwargs):
"""Open the cover."""
_LOGGER.debug("%s: open: updown = 0", self.name)
self.set_int_value('updown', 0)
def close_cover(self, **kwargs):
"""Close the cover."""
_LOGGER.debug("%s: open: updown = 1", self.name)
self.set_int_value('updown', 1)
def stop_cover(self, **kwargs):
"""Stop the cover movement."""
_LOGGER.debug("%s: stop: stop = 1", self.name)
self.set_int_value('stop', 1)
def set_cover_tilt_position(self, tilt_position, **kwargs):
"""Move the cover til to a specific position."""
if self._invert_angle:
tilt_position = 100-tilt_position
self._target_tilt = round(tilt_position, -1)
self.set_percentage('setangle', tilt_position)
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._device_class
@staticmethod
def to_knx_position(value, invert):
"""Convert hass position [100...0] to KNX position [0...255]."""
knx_position = round(value/100*255.4)
if not invert:
knx_position = 255-knx_position
print(value, " -> ", knx_position)
return knx_position
+15 -14
View File
@@ -1,14 +1,14 @@
"""
Support for Lutron Caseta SerenaRollerShade.
Support for Lutron Caseta shades.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.lutron_caseta/
"""
import logging
from homeassistant.components.cover import (
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION)
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION,
ATTR_POSITION, DOMAIN)
from homeassistant.components.lutron_caseta import (
LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice)
@@ -19,11 +19,10 @@ DEPENDENCIES = ['lutron_caseta']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lutron Caseta Serena shades as a cover device."""
"""Set up the Lutron Caseta shades as a cover device."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade",
"SerenaHoneycombShade"])
cover_devices = bridge.get_devices_by_domain(DOMAIN)
for cover_device in cover_devices:
dev = LutronCasetaCover(cover_device, bridge)
devs.append(dev)
@@ -32,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
"""Representation of a Lutron Serena shade."""
"""Representation of a Lutron shade."""
@property
def supported_features(self):
@@ -42,24 +41,26 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._state["current_state"] < 1
return self._state['current_state'] < 1
@property
def current_cover_position(self):
"""Return the current position of cover."""
return self._state["current_state"]
return self._state['current_state']
def close_cover(self):
def close_cover(self, **kwargs):
"""Close the cover."""
self._smartbridge.set_value(self._device_id, 0)
def open_cover(self):
def open_cover(self, **kwargs):
"""Open the cover."""
self._smartbridge.set_value(self._device_id, 100)
def set_cover_position(self, position, **kwargs):
"""Move the roller shutter to a specific position."""
self._smartbridge.set_value(self._device_id, position)
def set_cover_position(self, **kwargs):
"""Move the shade to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
self._smartbridge.set_value(self._device_id, position)
def update(self):
"""Call when forcing a refresh of the device."""
+2 -2
View File
@@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up the RFXtrx cover."""
import RFXtrx as rfxtrxmod
covers = rfxtrx.get_devices_from_config(config, RfxtrxCover, hass)
covers = rfxtrx.get_devices_from_config(config, RfxtrxCover)
add_devices_callback(covers)
def cover_update(event):
@@ -26,7 +26,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
not event.device.known_to_be_rollershutter:
return
new_device = rfxtrx.get_new_device(event, config, RfxtrxCover, hass)
new_device = rfxtrx.get_new_device(event, config, RfxtrxCover)
if new_device:
add_devices_callback([new_device])
+6 -6
View File
@@ -24,10 +24,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class XiaomiGenericCover(XiaomiDevice, CoverDevice):
"""Representation of a XiaomiPlug."""
"""Representation of a XiaomiGenericCover."""
def __init__(self, device, name, data_key, xiaomi_hub):
"""Initialize the XiaomiPlug."""
"""Initialize the XiaomiGenericCover."""
self._data_key = data_key
self._pos = 0
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
@@ -44,19 +44,19 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice):
def close_cover(self, **kwargs):
"""Close the cover."""
self._write_to_hub(self._sid, self._data_key['status'], 'close')
self._write_to_hub(self._sid, **{self._data_key['status']: 'close'})
def open_cover(self, **kwargs):
"""Open the cover."""
self._write_to_hub(self._sid, self._data_key['status'], 'open')
self._write_to_hub(self._sid, **{self._data_key['status']: 'open'})
def stop_cover(self, **kwargs):
"""Stop the cover."""
self._write_to_hub(self._sid, self._data_key['status'], 'stop')
self._write_to_hub(self._sid, **{self._data_key['status']: 'stop'})
def set_cover_position(self, position, **kwargs):
"""Move the cover to a specific position."""
self._write_to_hub(self._sid, self._data_key['pos'], str(position))
self._write_to_hub(self._sid, **{self._data_key['pos']: str(position)})
def parse_data(self, data):
"""Parse data sent by gateway."""
@@ -23,7 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
REQUIREMENTS = ['aioautomatic==0.6.0']
REQUIREMENTS = ['aioautomatic==0.6.2']
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
@@ -205,6 +205,7 @@ class AutomaticData(object):
self.hass = hass
self.devices = devices
self.vehicle_info = {}
self.vehicle_seen = {}
self.client = client
self.session = session
self.async_see = async_see
@@ -236,6 +237,14 @@ class AutomaticData(object):
return
yield from self.get_vehicle_info(vehicle)
if event.created_at < self.vehicle_seen[event.vehicle.id]:
# Skip events received out of order
_LOGGER.debug("Skipping out of order event. Event Created %s. "
"Last seen event: %s.", event.created_at,
self.vehicle_seen[event.vehicle.id])
return
self.vehicle_seen[event.vehicle.id] = event.created_at
kwargs = self.vehicle_info[event.vehicle.id]
if kwargs is None:
# Ignored device
@@ -323,15 +332,17 @@ class AutomaticData(object):
if self.devices is not None and name not in self.devices:
self.vehicle_info[vehicle.id] = None
return
else:
self.vehicle_info[vehicle.id] = kwargs = {
ATTR_DEV_ID: vehicle.id,
ATTR_HOST_NAME: name,
ATTR_MAC: vehicle.id,
ATTR_ATTRIBUTES: {
ATTR_FUEL_LEVEL: vehicle.fuel_level_percent,
}
self.vehicle_info[vehicle.id] = kwargs = {
ATTR_DEV_ID: vehicle.id,
ATTR_HOST_NAME: name,
ATTR_MAC: vehicle.id,
ATTR_ATTRIBUTES: {
ATTR_FUEL_LEVEL: vehicle.fuel_level_percent,
}
}
self.vehicle_seen[vehicle.id] = \
vehicle.updated_at or vehicle.created_at
if vehicle.latest_location is not None:
location = vehicle.latest_location
@@ -352,4 +363,7 @@ class AutomaticData(object):
kwargs[ATTR_GPS] = (location.lat, location.lon)
kwargs[ATTR_GPS_ACCURACY] = location.accuracy_m
if trips[0].ended_at >= self.vehicle_seen[vehicle.id]:
self.vehicle_seen[vehicle.id] = trips[0].ended_at
return kwargs
+127
View File
@@ -0,0 +1,127 @@
"""
Support for the Geofency platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.geofency/
"""
import asyncio
from functools import partial
import logging
import voluptuous as vol
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
ATTR_LATITUDE, ATTR_LONGITUDE, HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
BEACON_DEV_PREFIX = 'beacon'
CONF_MOBILE_BEACONS = 'mobile_beacons'
LOCATION_ENTRY = '1'
LOCATION_EXIT = '0'
URL = '/api/geofency'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MOBILE_BEACONS): vol.All(
cv.ensure_list, [cv.string]),
})
def setup_scanner(hass, config, see, discovery_info=None):
"""Set up an endpoint for the Geofency application."""
mobile_beacons = config.get(CONF_MOBILE_BEACONS) or []
hass.http.register_view(GeofencyView(see, mobile_beacons))
return True
class GeofencyView(HomeAssistantView):
"""View to handle Geofency requests."""
url = URL
name = 'api:geofency'
def __init__(self, see, mobile_beacons):
"""Initialize Geofency url endpoints."""
self.see = see
self.mobile_beacons = [slugify(beacon) for beacon in mobile_beacons]
@asyncio.coroutine
def post(self, request):
"""Handle Geofency requests."""
data = yield from request.post()
hass = request.app['hass']
data = self._validate_data(data)
if not data:
return ("Invalid data", HTTP_UNPROCESSABLE_ENTITY)
if self._is_mobile_beacon(data):
return (yield from self._set_location(hass, data, None))
else:
if data['entry'] == LOCATION_ENTRY:
location_name = data['name']
else:
location_name = STATE_NOT_HOME
return (yield from self._set_location(hass, data, location_name))
@staticmethod
def _validate_data(data):
"""Validate POST payload."""
data = data.copy()
required_attributes = ['address', 'device', 'entry',
'latitude', 'longitude', 'name']
valid = True
for attribute in required_attributes:
if attribute not in data:
valid = False
_LOGGER.error("'%s' not specified in message", attribute)
if not valid:
return False
data['address'] = data['address'].replace('\n', ' ')
data['device'] = slugify(data['device'])
data['name'] = slugify(data['name'])
data[ATTR_LATITUDE] = float(data[ATTR_LATITUDE])
data[ATTR_LONGITUDE] = float(data[ATTR_LONGITUDE])
return data
def _is_mobile_beacon(self, data):
"""Check if we have a mobile beacon."""
return 'beaconUUID' in data and data['name'] in self.mobile_beacons
@staticmethod
def _device_name(data):
"""Return name of device tracker."""
if 'beaconUUID' in data:
return "{}_{}".format(BEACON_DEV_PREFIX, data['name'])
else:
return data['device']
@asyncio.coroutine
def _set_location(self, hass, data, location_name):
"""Fire HA event to set location."""
device = self._device_name(data)
yield from hass.async_add_job(
partial(self.see, dev_id=device,
gps=(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
location_name=location_name,
attributes=data))
return "Setting location for {}".format(device)
@@ -307,12 +307,15 @@ class Icloud(DeviceScanner):
self.api.authenticate()
currentminutes = dt_util.now().hour * 60 + dt_util.now().minute
for devicename in self.devices:
interval = self._intervals.get(devicename, 1)
if ((currentminutes % interval == 0) or
(interval > 10 and
currentminutes % interval in [2, 4])):
self.update_device(devicename)
try:
for devicename in self.devices:
interval = self._intervals.get(devicename, 1)
if ((currentminutes % interval == 0) or
(interval > 10 and
currentminutes % interval in [2, 4])):
self.update_device(devicename)
except ValueError:
_LOGGER.debug("iCloud API returned an error")
def determine_interval(self, devicename, latitude, longitude, battery):
"""Calculate new interval."""
@@ -397,7 +400,7 @@ class Icloud(DeviceScanner):
self.see(**kwargs)
self.seen_devices[devicename] = True
except PyiCloudNoDevicesException:
_LOGGER.error('No iCloud Devices found!')
_LOGGER.error("No iCloud Devices found")
def lost_iphone(self, devicename):
"""Call the lost iPhone function if the device is found."""
@@ -0,0 +1,57 @@
"""
Support for the Tesla platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.tesla/
"""
import logging
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['tesla']
def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Tesla tracker."""
TeslaDeviceTracker(
hass, config, see,
hass.data[TESLA_DOMAIN]['devices']['devices_tracker'])
return True
class TeslaDeviceTracker(object):
"""A class representing a Tesla device."""
def __init__(self, hass, config, see, tesla_devices):
"""Initialize the Tesla device scanner."""
self.hass = hass
self.see = see
self.devices = tesla_devices
self._update_info()
track_utc_time_change(
self.hass, self._update_info, second=range(0, 60, 30))
def _update_info(self, now=None):
"""Update the device info."""
for device in self.devices:
device.update()
name = device.name
_LOGGER.debug("Updating device position: %s", name)
dev_id = slugify(device.uniq_name)
location = device.get_location()
lat = location['latitude']
lon = location['longitude']
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': name
}
self.see(
dev_id=dev_id, host_name=name,
gps=(lat, lon), attributes=attrs
)
@@ -20,11 +20,12 @@ def setup_scanner(hass, config, see, discovery_info=None):
return
vin, _ = discovery_info
vehicle = hass.data[DATA_KEY].vehicles[vin]
voc = hass.data[DATA_KEY]
vehicle = voc.vehicles[vin]
def see_vehicle(vehicle):
"""Handle the reporting of the vehicle position."""
host_name = vehicle.registration_number
host_name = voc.vehicle_name(vehicle)
dev_id = 'volvo_{}'.format(slugify(host_name))
see(dev_id=dev_id,
host_name=host_name,
+1
View File
@@ -100,6 +100,7 @@ def async_setup(hass, config):
# We do not know how to handle this service.
if not comp_plat:
logger.info("Unknown service discovered: %s %s", service, info)
return
discovery_hash = json.dumps([service, info], sort_keys=True)
+1 -1
View File
@@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle
REQUIREMENTS = ['python-ecobee-api==0.0.8']
REQUIREMENTS = ['python-ecobee-api==0.0.9']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
+33 -11
View File
@@ -28,6 +28,7 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/')
ATTR_THEMES = 'themes'
ATTR_EXTRA_HTML_URL = 'extra_html_url'
DEFAULT_THEME_COLOR = '#03A9F4'
MANIFEST_JSON = {
'background_color': '#FFFFFF',
@@ -50,6 +51,7 @@ for size in (192, 384, 512, 1024):
})
DATA_PANELS = 'frontend_panels'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
DATA_INDEX_VIEW = 'frontend_index_view'
DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme'
@@ -66,6 +68,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(ATTR_THEMES): vol.Schema({
cv.string: {cv.string: cv.string}
}),
vol.Optional(ATTR_EXTRA_HTML_URL):
vol.All(cv.ensure_list, [cv.string]),
}),
}, extra=vol.ALLOW_EXTRA)
@@ -105,14 +109,13 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
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, 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)
url: for the web component (for dev environment, optional)
url: for the web component (optional)
config: config to be passed into the web component
Warning: this API will probably change. Use at own risk.
"""
panels = hass.data.get(DATA_PANELS)
if panels is None:
@@ -123,14 +126,16 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
if url_path in panels:
_LOGGER.warning("Overwriting component %s", url_path)
if not os.path.isfile(path):
_LOGGER.error(
"Panel %s component does not exist: %s", component_name, path)
return
if md5 is None:
with open(path) as fil:
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
if url is None:
if not os.path.isfile(path):
_LOGGER.error(
"Panel %s component does not exist: %s", component_name, path)
return
if md5 is None:
with open(path) as fil:
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
data = {
'url_path': url_path,
@@ -169,6 +174,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
'get', '/{}/{{extra:.+}}'.format(url_path), index_view.get)
@bind_hass
def add_extra_html_url(hass, url):
"""Register extra html url to load."""
url_set = hass.data.get(DATA_EXTRA_HTML_URL)
if url_set is None:
url_set = hass.data[DATA_EXTRA_HTML_URL] = set()
url_set.add(url)
def add_manifest_json_key(key, val):
"""Add a keyval to the manifest.json."""
MANIFEST_JSON[key] = val
@@ -208,6 +222,9 @@ def setup(hass, config):
else:
hass.data[DATA_PANELS] = {}
if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set()
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
@@ -217,6 +234,9 @@ def setup(hass, config):
themes = config.get(DOMAIN, {}).get(ATTR_THEMES)
setup_themes(hass, themes)
for url in config.get(DOMAIN, {}).get(ATTR_EXTRA_HTML_URL, []):
add_extra_html_url(hass, url)
return True
@@ -362,7 +382,9 @@ class IndexView(HomeAssistantView):
compatibility_url=compatibility_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url, panels=hass.data[DATA_PANELS],
dev_mode=request.app[KEY_DEVELOPMENT])
dev_mode=request.app[KEY_DEVELOPMENT],
theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[DATA_EXTRA_HTML_URL])
return web.Response(text=resp, content_type='text/html')
@@ -21,7 +21,7 @@
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='#03a9f4'>
<meta name='theme-color' content='{{ theme_color }}'>
<style>
body {
font-family: 'Roboto', 'Noto', sans-serif;
@@ -97,6 +97,10 @@
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
{% endif -%}
<link rel='import' href='{{ icons_url }}' async>
{% for extra_url in extra_urls -%}
<link rel='import' href='{{ extra_url }}' async>
{% endfor -%}
<script>
var webComponentsSupported = (
'registerElement' in document &&
+5 -5
View File
@@ -3,22 +3,22 @@
FINGERPRINTS = {
"compatibility.js": "1686167ff210e001f063f5c606b2e74b",
"core.js": "2a7d01e45187c7d4635da05065b5e54e",
"frontend.html": "6c8192a4393c9e83516dc8177b75c23d",
"mdi.html": "e91f61a039ed0a9936e7ee5360da3870",
"frontend.html": "6b0a95408d9ee869d0fe20c374077ed4",
"mdi.html": "89074face5529f5fe6fbae49ecb3e88b",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-config.html": "bd20a3b11b46522e3c705a0b6a72b9dc",
"panels/ha-panel-config.html": "0b985cbf668b16bca9f34727036c7139",
"panels/ha-panel-dev-event.html": "d409e7ab537d9fe629126d122345279c",
"panels/ha-panel-dev-info.html": "b0e55eb657fd75f21aba2426ac0cedc0",
"panels/ha-panel-dev-mqtt.html": "94b222b013a98583842de3e72d5888c6",
"panels/ha-panel-dev-service.html": "422b2c181ee0713fa31d45a64e605baf",
"panels/ha-panel-dev-state.html": "7948d3dba058f31517d880df8ed0e857",
"panels/ha-panel-dev-template.html": "f47b6910d8e4880e22cc508ca452f9b6",
"panels/ha-panel-dev-template.html": "928e7b81b9c113b70edc9f4a1d051827",
"panels/ha-panel-hassio.html": "b46e7619f3c355f872d5370741d89f6a",
"panels/ha-panel-history.html": "fe2daac10a14f51fa3eb7d23978df1f7",
"panels/ha-panel-iframe.html": "56930204d6e067a3d600cf030f4b34c8",
"panels/ha-panel-kiosk.html": "b40aa5cb52dd7675bea744afcf9eebf8",
"panels/ha-panel-logbook.html": "771afdcf48dc7e308b0282417d2e02d8",
"panels/ha-panel-mailbox.html": "a8cca44ca36553e91565e3c894ea6323",
"panels/ha-panel-map.html": "c2544fff3eedb487d44105cf94b335ec",
"panels/ha-panel-map.html": "565db019147162080c21af962afc097f",
"panels/ha-panel-shopping-list.html": "d8cfd0ecdb3aa6214c0f6908c34c7141"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
<html><head></head><body><dom-module id="ha-panel-dev-template"><template><style is="custom-style" include="ha-style iron-flex iron-positioning"></style><style>:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial;}.content{padding:16px;}.edit-pane{margin-right:16px;}.edit-pane a{color:var(--dark-primary-color);}.horizontal .edit-pane{max-width:50%;}.render-pane{position:relative;max-width:50%;}.render-spinner{position:absolute;top:8px;right:8px;}.rendered{@apply (--paper-font-code1)
clear: both;white-space:pre-wrap;}.rendered.error{color:red;}</style><app-header-layout has-scrolling-region=""><app-header slot="header" fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Templates</div></app-toolbar></app-header><div class$="[[computeFormClasses(narrow)]]"><div class="edit-pane"><p>Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.</p><ul><li><a href="http://jinja.pocoo.org/docs/dev/templates/" target="_blank">Jinja2 template documentation</a></li><li><a href="https://home-assistant.io/topics/templating/" target="_blank">Home Assistant template extensions</a></li></ul><paper-textarea label="Template" value="{{template}}"></paper-textarea></div><div class="render-pane"><paper-spinner class="render-spinner" active="[[rendering]]"></paper-spinner><pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre></div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-template",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},error:{type:Boolean,value:!1},rendering:{type:Boolean,value:!1},template:{type:String,value:'Imitate available variables:\n{% set my_test_json = {\n "temperature": 25,\n "unit": "°C"\n} %}\n\nThe temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. \n\n{% if is_state("device_tracker.paulus", "home") and \n is_state("device_tracker.anne_therese", "home") -%}\n\n You are both home, you silly\n\n{%- else -%}\n\n Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}\n\n{%- endif %}\n\nFor loop example:\n{% for state in states.sensor -%}\n {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}\n {{ state.name | lower }} is {{state.state}} {{- state.attributes.unit_of_measurement}}\n{%- endfor -%}.',observer:"templateChanged"},processed:{type:String,value:""}},computeFormClasses:function(e){return e?"content fit":"content fit layout horizontal"},computeRenderedClasses:function(e){return e?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate.bind(this),500)},renderTemplate:function(){this.rendering=!0,this.hass.callApi("POST","template",{template:this.template}).then(function(e){this.processed=e,this.rendering=!1}.bind(this),function(e){this.processed=e.body.message,this.error=!0,this.rendering=!1}.bind(this))}});</script></body></html>
clear: both;white-space:pre-wrap;}.rendered.error{color:red;}</style><app-header-layout has-scrolling-region=""><app-header slot="header" fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Templates</div></app-toolbar></app-header><div class$="[[computeFormClasses(narrow)]]"><div class="edit-pane"><p>Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.</p><ul><li><a href="http://jinja.pocoo.org/docs/dev/templates/" target="_blank">Jinja2 template documentation</a></li><li><a href="https://home-assistant.io/topics/templating/" target="_blank">Home Assistant template extensions</a></li></ul><paper-textarea label="Template" value="{{template}}"></paper-textarea></div><div class="render-pane"><paper-spinner class="render-spinner" active="[[rendering]]"></paper-spinner><pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre></div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-template",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},error:{type:Boolean,value:!1},rendering:{type:Boolean,value:!1},template:{type:String,value:'Imitate available variables:\n{% set my_test_json = {\n "temperature": 25,\n "unit": "°C"\n} %}\n\nThe temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. \n\n{% if is_state("device_tracker.paulus", "home") and \n is_state("device_tracker.anne_therese", "home") -%}\n\n You are both home, you silly\n\n{%- else -%}\n\n Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}\n\n{%- endif %}\n\nFor loop example:\n{% for state in states.sensor -%}\n {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}\n {{ state.name | lower }} is {{state.state_with_unit}}\n{%- endfor -%}.',observer:"templateChanged"},processed:{type:String,value:""}},computeFormClasses:function(e){return e?"content fit":"content fit layout horizontal"},computeRenderedClasses:function(e){return e?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate.bind(this),500)},renderTemplate:function(){this.rendering=!0,this.hass.callApi("POST","template",{template:this.template}).then(function(e){this.processed=e,this.rendering=!1}.bind(this),function(e){this.processed=e.body.message,this.error=!0,this.rendering=!1}.bind(this))}});</script></body></html>
File diff suppressed because one or more lines are too long
@@ -37,7 +37,7 @@
/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */
'use strict';
var precacheConfig = [["/","535d629ec4d3936dba0ca4ca84dabeb2"],["/frontend/panels/dev-event-d409e7ab537d9fe629126d122345279c.html","936814991f2a5e23d61d29f0d40f81b8"],["/frontend/panels/dev-info-b0e55eb657fd75f21aba2426ac0cedc0.html","1fa953b0224470f70d4e87bbe4dff191"],["/frontend/panels/dev-mqtt-94b222b013a98583842de3e72d5888c6.html","dc3ddfac58397feda97317358f0aecbb"],["/frontend/panels/dev-service-422b2c181ee0713fa31d45a64e605baf.html","ae7d26b1c8c3309fd3c65944f89ea03f"],["/frontend/panels/dev-state-7948d3dba058f31517d880df8ed0e857.html","ff8156bb1a52490fcc07466556fce0e1"],["/frontend/panels/dev-template-f47b6910d8e4880e22cc508ca452f9b6.html","9aa0675e01373c6bc2737438bb84a9ec"],["/frontend/panels/map-c2544fff3eedb487d44105cf94b335ec.html","113c5bf9a68a74c62e50cd354034e78b"],["/static/compatibility-1686167ff210e001f063f5c606b2e74b.js","6ee7b5e2dd82b510c3bd92f7e215988e"],["/static/core-2a7d01e45187c7d4635da05065b5e54e.js","90a0a8a6a6dd0ca41b16f40e7d23924d"],["/static/frontend-6c8192a4393c9e83516dc8177b75c23d.html","56d5bfe9e11a8b81a686f20aeae3c359"],["/static/mdi-e91f61a039ed0a9936e7ee5360da3870.html","5e587bc82719b740a4f0798722a83aee"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"]];
var precacheConfig = [["/","e22b4dfa3b4277935d374eb30b36b7a7"],["/frontend/panels/dev-event-d409e7ab537d9fe629126d122345279c.html","936814991f2a5e23d61d29f0d40f81b8"],["/frontend/panels/dev-info-b0e55eb657fd75f21aba2426ac0cedc0.html","1fa953b0224470f70d4e87bbe4dff191"],["/frontend/panels/dev-mqtt-94b222b013a98583842de3e72d5888c6.html","dc3ddfac58397feda97317358f0aecbb"],["/frontend/panels/dev-service-422b2c181ee0713fa31d45a64e605baf.html","ae7d26b1c8c3309fd3c65944f89ea03f"],["/frontend/panels/dev-state-7948d3dba058f31517d880df8ed0e857.html","ff8156bb1a52490fcc07466556fce0e1"],["/frontend/panels/dev-template-928e7b81b9c113b70edc9f4a1d051827.html","312c8313800b44c83bcb8dc2df30c759"],["/frontend/panels/map-565db019147162080c21af962afc097f.html","a1a360042395682335e2f471dddad309"],["/static/compatibility-1686167ff210e001f063f5c606b2e74b.js","6ee7b5e2dd82b510c3bd92f7e215988e"],["/static/core-2a7d01e45187c7d4635da05065b5e54e.js","90a0a8a6a6dd0ca41b16f40e7d23924d"],["/static/frontend-6b0a95408d9ee869d0fe20c374077ed4.html","2fced25e314a02654197adbfe36f1063"],["/static/mdi-89074face5529f5fe6fbae49ecb3e88b.html","97754e463f9e56a95c813d4d8e792347"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"]];
var cacheName = 'sw-precache-v3--' + (self.registration ? self.registration.scope : '');
+2 -2
View File
@@ -31,7 +31,7 @@ DOMAIN = 'hdmi_cec'
_LOGGER = logging.getLogger(__name__)
DEFAULT_DISPLAY_NAME = "HomeAssistant"
DEFAULT_DISPLAY_NAME = "HA"
CONF_TYPES = 'types'
ICON_UNKNOWN = 'mdi:help'
@@ -181,7 +181,7 @@ def setup(hass: HomeAssistant, base_config):
if host:
adapter = TcpAdapter(host, name=display_name, activate_source=False)
else:
adapter = CecAdapter(name=display_name, activate_source=False)
adapter = CecAdapter(name=display_name[:12], activate_source=False)
hdmi_network = HDMINetwork(adapter, loop=loop)
def _volume(call):
+29 -28
View File
@@ -4,8 +4,8 @@ Support for HomeMatic devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematic/
"""
import asyncio
import os
import time
import logging
from datetime import timedelta
from functools import partial
@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID)
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyhomematic==0.1.30']
@@ -121,7 +121,6 @@ CONF_RESOLVENAMES_OPTIONS = [
]
DATA_HOMEMATIC = 'homematic'
DATA_DELAY = 'homematic_delay'
DATA_DEVINIT = 'homematic_devinit'
DATA_STORE = 'homematic_store'
@@ -134,7 +133,6 @@ CONF_CALLBACK_PORT = 'callback_port'
CONF_RESOLVENAMES = 'resolvenames'
CONF_VARIABLES = 'variables'
CONF_DEVICES = 'devices'
CONF_DELAY = 'delay'
CONF_PRIMARY = 'primary'
DEFAULT_LOCAL_IP = '0.0.0.0'
@@ -145,7 +143,6 @@ DEFAULT_USERNAME = 'Admin'
DEFAULT_PASSWORD = ''
DEFAULT_VARIABLES = False
DEFAULT_DEVICES = True
DEFAULT_DELAY = 0.5
DEFAULT_PRIMARY = False
@@ -177,7 +174,6 @@ CONFIG_SCHEMA = vol.Schema({
}},
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
}),
}, extra=vol.ALLOW_EXTRA)
@@ -249,7 +245,6 @@ def setup(hass, config):
"""Set up the Homematic component."""
from pyhomematic import HMConnection
hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY)
hass.data[DATA_DEVINIT] = {}
hass.data[DATA_STORE] = set()
@@ -277,7 +272,7 @@ def setup(hass, config):
# Create server thread
bound_system_callback = partial(_system_callback_handler, hass, config)
hass.data[DATA_HOMEMATIC] = HMConnection(
hass.data[DATA_HOMEMATIC] = homematic = HMConnection(
local=config[DOMAIN].get(CONF_LOCAL_IP),
localport=config[DOMAIN].get(CONF_LOCAL_PORT),
remotes=remotes,
@@ -286,7 +281,7 @@ def setup(hass, config):
)
# Start server thread, connect to hosts, initialize to receive events
hass.data[DATA_HOMEMATIC].start()
homematic.start()
# Stops server when HASS is shutting down
hass.bus.listen_once(
@@ -296,7 +291,7 @@ def setup(hass, config):
entity_hubs = []
for _, hub_data in hosts.items():
entity_hubs.append(HMHub(
hass, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
homematic, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
# Register HomeMatic services
descriptions = load_yaml_config_file(
@@ -359,7 +354,7 @@ def setup(hass, config):
def _service_handle_reconnect(service):
"""Service to reconnect all HomeMatic hubs."""
hass.data[DATA_HOMEMATIC].reconnect()
homematic.reconnect()
hass.services.register(
DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect,
@@ -575,24 +570,27 @@ def _device_from_servicecall(hass, service):
class HMHub(Entity):
"""The HomeMatic hub. (CCU2/HomeGear)."""
def __init__(self, hass, name, use_variables):
def __init__(self, homematic, name, use_variables):
"""Initialize HomeMatic hub."""
self.hass = hass
self.entity_id = "{}.{}".format(DOMAIN, name.lower())
self._homematic = hass.data[DATA_HOMEMATIC]
self._homematic = homematic
self._variables = {}
self._name = name
self._state = STATE_UNKNOWN
self._use_variables = use_variables
@asyncio.coroutine
def async_added_to_hass(self):
"""Load data init callbacks."""
# Load data
track_time_interval(hass, self._update_hub, SCAN_INTERVAL_HUB)
self._update_hub(None)
async_track_time_interval(
self.hass, self._update_hub, SCAN_INTERVAL_HUB)
yield from self.hass.async_add_job(self._update_hub, None)
if self._use_variables:
track_time_interval(
hass, self._update_variables, SCAN_INTERVAL_VARIABLES)
self._update_variables(None)
async_track_time_interval(
self.hass, self._update_variables, SCAN_INTERVAL_VARIABLES)
yield from self.hass.async_add_job(self._update_variables, None)
@property
def name(self):
@@ -624,7 +622,9 @@ class HMHub(Entity):
"""Retrieve latest state."""
state = self._homematic.getServiceMessages(self._name)
self._state = STATE_UNKNOWN if state is None else len(state)
self.schedule_update_ha_state()
if now:
self.schedule_update_ha_state()
def _update_variables(self, now):
"""Retrive all variable data and update hmvariable states."""
@@ -640,7 +640,7 @@ class HMHub(Entity):
state_change = True
self._variables.update({key: value})
if state_change:
if state_change and now:
self.schedule_update_ha_state()
def hm_set_variable(self, name, value):
@@ -662,16 +662,15 @@ class HMHub(Entity):
class HMDevice(Entity):
"""The HomeMatic device base object."""
def __init__(self, hass, config):
def __init__(self, config):
"""Initialize a generic HomeMatic device."""
self.hass = hass
self._homematic = hass.data[DATA_HOMEMATIC]
self._name = config.get(ATTR_NAME)
self._address = config.get(ATTR_ADDRESS)
self._proxy = config.get(ATTR_PROXY)
self._channel = config.get(ATTR_CHANNEL)
self._state = config.get(ATTR_PARAM)
self._data = {}
self._homematic = None
self._hmdevice = None
self._connected = False
self._available = False
@@ -680,6 +679,11 @@ class HMDevice(Entity):
if self._state:
self._state = self._state.upper()
@asyncio.coroutine
def async_added_to_hass(self):
"""Load data init callbacks."""
yield from self.hass.async_add_job(self.link_homematic)
@property
def should_poll(self):
"""Return false. HomeMatic states are pushed by the XML-RPC Server."""
@@ -728,16 +732,13 @@ class HMDevice(Entity):
return True
# Initialize
self._homematic = self.hass.data[DATA_HOMEMATIC]
self._hmdevice = self._homematic.devices[self._proxy][self._address]
self._connected = True
try:
# Initialize datapoints of this object
self._init_data()
if self.hass.data[DATA_DELAY]:
# We optionally delay / pause loading of data to avoid
# overloading of CCU / Homegear
time.sleep(self.hass.data[DATA_DELAY])
self._load_data_from_hm()
# Link events from pyhomematic
@@ -15,7 +15,7 @@ from homeassistant.components.image_processing import (
from homeassistant.components.image_processing.microsoft_face_identify import (
ImageProcessingFaceEntity)
REQUIREMENTS = ['face_recognition==0.2.2']
REQUIREMENTS = ['face_recognition==1.0.0']
_LOGGER = logging.getLogger(__name__)
@@ -16,7 +16,7 @@ from homeassistant.components.image_processing.microsoft_face_identify import (
ImageProcessingFaceEntity)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['face_recognition==0.2.2']
REQUIREMENTS = ['face_recognition==1.0.0']
_LOGGER = logging.getLogger(__name__)
+191
View File
@@ -0,0 +1,191 @@
"""
Component to offer a way to enter a value into a text box.
For more details about this component, please refer to the documentation
at https://home-assistant.io/components/input_text/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME)
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'input_text'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
CONF_INITIAL = 'initial'
CONF_MIN = 'min'
CONF_MAX = 'max'
ATTR_VALUE = 'value'
ATTR_MIN = 'min'
ATTR_MAX = 'max'
ATTR_PATTERN = 'pattern'
SERVICE_SET_VALUE = 'set_value'
SERVICE_SET_VALUE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_VALUE): cv.string,
})
def _cv_input_text(cfg):
"""Configure validation helper for input box (voluptuous)."""
minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX)
if minimum > maximum:
raise vol.Invalid('Max len ({}) is not greater than min len ({})'
.format(minimum, maximum))
state = cfg.get(CONF_INITIAL)
if state is not None and (len(state) < minimum or len(state) > maximum):
raise vol.Invalid('Initial value {} length not in range {}-{}'
.format(state, minimum, maximum))
return cfg
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN, default=0): vol.Coerce(int),
vol.Optional(CONF_MAX, default=100): vol.Coerce(int),
vol.Optional(CONF_INITIAL, ''): cv.string,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(ATTR_PATTERN): cv.string,
}, _cv_input_text)
})
}, required=True, extra=vol.ALLOW_EXTRA)
@bind_hass
def set_value(hass, entity_id, value):
"""Set input_text to value."""
hass.services.call(DOMAIN, SERVICE_SET_VALUE, {
ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: value,
})
@asyncio.coroutine
def async_setup(hass, config):
"""Set up an input text box."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
entities = []
for object_id, cfg in config[DOMAIN].items():
name = cfg.get(CONF_NAME)
minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX)
initial = cfg.get(CONF_INITIAL)
icon = cfg.get(CONF_ICON)
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
pattern = cfg.get(ATTR_PATTERN)
entities.append(InputText(
object_id, name, initial, minimum, maximum, icon, unit,
pattern))
if not entities:
return False
@asyncio.coroutine
def async_set_value_service(call):
"""Handle a calls to the input box services."""
target_inputs = component.async_extract_from_service(call)
tasks = [input_text.async_set_value(call.data[ATTR_VALUE])
for input_text in target_inputs]
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_VALUE, async_set_value_service,
schema=SERVICE_SET_VALUE_SCHEMA)
yield from component.async_add_entities(entities)
return True
class InputText(Entity):
"""Represent a text box."""
def __init__(self, object_id, name, initial, minimum, maximum, icon,
unit, pattern):
"""Initialize a text input."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
self._current_value = initial
self._minimum = minimum
self._maximum = maximum
self._icon = icon
self._unit = unit
self._pattern = pattern
@property
def should_poll(self):
"""If entity should be polled."""
return False
@property
def name(self):
"""Return the name of the text input entity."""
return self._name
@property
def icon(self):
"""Return the icon to be used for this entity."""
return self._icon
@property
def state(self):
"""Return the state of the component."""
return self._current_value
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit
@property
def state_attributes(self):
"""Return the state attributes."""
return {
ATTR_MIN: self._minimum,
ATTR_MAX: self._maximum,
ATTR_PATTERN: self._pattern,
}
@asyncio.coroutine
def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
if self._current_value is not None:
return
state = yield from async_get_last_state(self.hass, self.entity_id)
value = state and state.state
# Check against None because value can be 0
if value is not None and self._minimum <= len(value) <= self._maximum:
self._current_value = value
@asyncio.coroutine
def async_set_value(self, value):
"""Select new value."""
if len(value) < self._minimum or len(value) > self._maximum:
_LOGGER.warning("Invalid value: %s (length range %s - %s)",
value, self._minimum, self._maximum)
return
self._current_value = value
yield from self.async_update_ha_state()
+1 -1
View File
@@ -102,7 +102,7 @@ def common_attributes(entity):
'address': 'INSTEON Address',
'description': 'Description',
'model': 'Model',
'cat': 'Cagegory',
'cat': 'Category',
'subcat': 'Subcategory',
'firmware': 'Firmware',
'product_key': 'Product Key'
+1 -1
View File
@@ -17,7 +17,7 @@ from homeassistant.helpers import discovery, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, Dict # noqa
REQUIREMENTS = ['PyISY==1.0.7']
REQUIREMENTS = ['PyISY==1.0.8']
_LOGGER = logging.getLogger(__name__)
+201 -441
View File
@@ -1,495 +1,255 @@
"""
Support for KNX components.
For more details about this component, please refer to the documentation at
Connects to KNX platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/knx/
"""
import logging
import os
import asyncio
import voluptuous as vol
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT)
from homeassistant.helpers.entity import Entity
from homeassistant.config import load_yaml_config_file
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \
CONF_HOST, CONF_PORT
from homeassistant.helpers.script import Script
REQUIREMENTS = ['knxip==0.5']
DOMAIN = "knx"
DATA_KNX = "data_knx"
CONF_KNX_CONFIG = "config_file"
CONF_KNX_ROUTING = "routing"
CONF_KNX_TUNNELING = "tunneling"
CONF_KNX_LOCAL_IP = "local_ip"
CONF_KNX_FIRE_EVENT = "fire_event"
CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter"
SERVICE_KNX_SEND = "send"
SERVICE_KNX_ATTR_ADDRESS = "address"
SERVICE_KNX_ATTR_PAYLOAD = "payload"
ATTR_DISCOVER_DEVICES = 'devices'
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = '0.0.0.0'
DEFAULT_PORT = 3671
DOMAIN = 'knx'
REQUIREMENTS = ['xknx==0.7.13']
EVENT_KNX_FRAME_RECEIVED = 'knx_frame_received'
EVENT_KNX_FRAME_SEND = 'knx_frame_send'
TUNNELING_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Required(CONF_KNX_LOCAL_IP): cv.string,
})
KNXTUNNEL = None
KNX_ADDRESS = "address"
KNX_DATA = "data"
KNX_GROUP_WRITE = "group_write"
CONF_LISTEN = "listen"
ROUTING_SCHEMA = vol.Schema({
vol.Required(CONF_KNX_LOCAL_IP): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_LISTEN, default=[]):
vol.All(cv.ensure_list, [cv.string]),
}),
vol.Optional(CONF_KNX_CONFIG): cv.string,
vol.Exclusive(CONF_KNX_ROUTING, 'connection_type'): ROUTING_SCHEMA,
vol.Exclusive(CONF_KNX_TUNNELING, 'connection_type'):
TUNNELING_SCHEMA,
vol.Inclusive(CONF_KNX_FIRE_EVENT, 'fire_ev'):
cv.boolean,
vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'):
vol.All(
cv.ensure_list,
[cv.string])
})
}, extra=vol.ALLOW_EXTRA)
KNX_WRITE_SCHEMA = vol.Schema({
vol.Required(KNX_ADDRESS): vol.All(cv.ensure_list, [cv.string]),
vol.Required(KNX_DATA): vol.All(cv.ensure_list, [cv.byte])
SERVICE_KNX_SEND_SCHEMA = vol.Schema({
vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string,
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any(
cv.positive_int, [cv.positive_int]),
})
def setup(hass, config):
"""Set up the connection to the KNX IP interface."""
global KNXTUNNEL
from knxip.ip import KNXIPTunnel
from knxip.core import KNXException, parse_group_address
host = config[DOMAIN].get(CONF_HOST)
port = config[DOMAIN].get(CONF_PORT)
if host == '0.0.0.0':
_LOGGER.debug("Will try to auto-detect KNX/IP gateway")
KNXTUNNEL = KNXIPTunnel(host, port)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up knx component."""
from xknx.exceptions import XKNXException
try:
res = KNXTUNNEL.connect()
_LOGGER.debug("Res = %s", res)
if not res:
_LOGGER.error("Could not connect to KNX/IP interface %s", host)
return False
hass.data[DATA_KNX] = KNXModule(hass, config)
yield from hass.data[DATA_KNX].start()
except KNXException as ex:
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
KNXTUNNEL = None
except XKNXException as ex:
_LOGGER.exception("Can't connect to KNX interface: %s", ex)
return False
_LOGGER.info("KNX IP tunnel to %s:%i established", host, port)
for component, discovery_type in (
('switch', 'Switch'),
('climate', 'Climate'),
('cover', 'Cover'),
('light', 'Light'),
('sensor', 'Sensor'),
('binary_sensor', 'BinarySensor'),
('notify', 'Notification')):
found_devices = _get_devices(hass, discovery_type)
hass.async_add_job(
discovery.async_load_platform(hass, component, DOMAIN, {
ATTR_DISCOVER_DEVICES: found_devices
}, config))
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def received_knx_event(address, data):
"""Process received KNX message."""
if len(data) == 1:
data = data[0]
hass.bus.fire('knx_event', {
'address': address,
'data': data
})
for listen in config[DOMAIN].get(CONF_LISTEN):
_LOGGER.debug("Registering listener for %s", listen)
try:
KNXTUNNEL.register_listener(parse_group_address(listen),
received_knx_event)
except KNXException as knxexception:
_LOGGER.error("Can't register KNX listener for address %s (%s)",
listen, knxexception)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_tunnel)
# Listen to KNX events and send them to the bus
def handle_group_write(call):
"""Bridge knx_frame_send events to the KNX bus."""
# parameters are pre-validated using KNX_WRITE_SCHEMA
addrlist = call.data.get("address")
knxdata = call.data.get("data")
knxaddrlist = []
for addr in addrlist:
try:
_LOGGER.debug("Found %s", addr)
knxaddr = int(addr)
except ValueError:
knxaddr = None
if knxaddr is None:
try:
knxaddr = parse_group_address(addr)
except KNXException:
_LOGGER.error("KNX address format incorrect: %s", addr)
knxaddrlist.append(knxaddr)
for addr in knxaddrlist:
KNXTUNNEL.group_write(addr, knxdata)
# Listen for when knx_frame_send event is fired
hass.services.register(DOMAIN,
KNX_GROUP_WRITE,
handle_group_write,
descriptions[DOMAIN][KNX_GROUP_WRITE],
schema=KNX_WRITE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_KNX_SEND,
hass.data[DATA_KNX].service_send_to_knx_bus,
schema=SERVICE_KNX_SEND_SCHEMA)
return True
def close_tunnel(_data):
"""Close the NKX tunnel connection on shutdown."""
global KNXTUNNEL
KNXTUNNEL.disconnect()
KNXTUNNEL = None
def _get_devices(hass, discovery_type):
return list(
map(lambda device: device.name,
filter(
lambda device: type(device).__name__ == discovery_type,
hass.data[DATA_KNX].xknx.devices)))
class KNXConfig(object):
"""Handle the fetching of configuration from the config file."""
def __init__(self, config):
"""Initialize the configuration."""
from knxip.core import parse_group_address
self.config = config
self.should_poll = config.get('poll', True)
if config.get('address'):
self._address = parse_group_address(config.get('address'))
else:
self._address = None
if self.config.get('state_address'):
self._state_address = parse_group_address(
self.config.get('state_address'))
else:
self._state_address = None
@property
def name(self):
"""Return the name given to the entity."""
return self.config['name']
@property
def address(self):
"""Return the address of the device as an integer value.
3 types of addresses are supported:
integer - 0-65535
2 level - a/b
3 level - a/b/c
"""
return self._address
@property
def state_address(self):
"""Return the group address the device sends its current state to.
Some KNX devices can send the current state to a seperate
group address. This makes send e.g. when an actuator can
be switched but also have a timer functionality.
"""
return self._state_address
class KNXGroupAddress(Entity):
"""Representation of devices connected to a KNX group address."""
class KNXModule(object):
"""Representation of KNX Object."""
def __init__(self, hass, config):
"""Initialize the device."""
self._config = config
self._state = False
self._data = None
_LOGGER.debug(
"Initalizing KNX group address for %s (%s)",
self.name, self.address
)
"""Initialization of KNXModule."""
self.hass = hass
self.config = config
self.initialized = False
self.init_xknx()
self.register_callbacks()
def handle_knx_message(addr, data):
"""Handle an incoming KNX frame.
def init_xknx(self):
"""Initialization of KNX object."""
from xknx import XKNX
self.xknx = XKNX(
config=self.config_file(),
loop=self.hass.loop)
Handle an incoming frame and update our status if it contains
information relating to this device.
"""
if (addr == self.state_address) or (addr == self.address):
self._state = data[0]
self.schedule_update_ha_state()
@asyncio.coroutine
def start(self):
"""Start KNX object. Connect to tunneling or Routing device."""
connection_config = self.connection_config()
yield from self.xknx.start(
state_updater=True,
connection_config=connection_config)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
self.initialized = True
KNXTUNNEL.register_listener(self.address, handle_knx_message)
if self.state_address:
KNXTUNNEL.register_listener(self.state_address, handle_knx_message)
@asyncio.coroutine
def stop(self, event):
"""Stop KNX object. Disconnect from tunneling or Routing device."""
yield from self.xknx.stop()
@property
def name(self):
"""Return the entity's display name."""
return self._config.name
def config_file(self):
"""Resolve and return the full path of xknx.yaml if configured."""
config_file = self.config[DOMAIN].get(CONF_KNX_CONFIG)
if not config_file:
return None
if not config_file.startswith("/"):
return self.hass.config.path(config_file)
return config_file
@property
def config(self):
"""Return the entity's configuration."""
return self._config
def connection_config(self):
"""Return the connection_config."""
if CONF_KNX_TUNNELING in self.config[DOMAIN]:
return self.connection_config_tunneling()
elif CONF_KNX_ROUTING in self.config[DOMAIN]:
return self.connection_config_routing()
return self.connection_config_auto()
@property
def should_poll(self):
"""Return the state of the polling, if needed."""
return self._config.should_poll
def connection_config_routing(self):
"""Return the connection_config if routing is configured."""
from xknx.io import ConnectionConfig, ConnectionType
local_ip = \
self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP)
return ConnectionConfig(
connection_type=ConnectionType.ROUTING,
local_ip=local_ip)
@property
def is_on(self):
"""Return True if the value is not 0 is on, else False."""
return self._state != 0
def connection_config_tunneling(self):
"""Return the connection_config if tunneling is configured."""
from xknx.io import ConnectionConfig, ConnectionType, \
DEFAULT_MCAST_PORT
gateway_ip = \
self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST)
gateway_port = \
self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT)
local_ip = \
self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP)
if gateway_port is None:
gateway_port = DEFAULT_MCAST_PORT
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
gateway_ip=gateway_ip,
gateway_port=gateway_port,
local_ip=local_ip)
@property
def address(self):
"""Return the KNX group address."""
return self._config.address
def connection_config_auto(self):
"""Return the connection_config if auto is configured."""
# pylint: disable=no-self-use
from xknx.io import ConnectionConfig
return ConnectionConfig()
@property
def state_address(self):
"""Return the KNX group address."""
return self._config.state_address
def register_callbacks(self):
"""Register callbacks within XKNX object."""
if CONF_KNX_FIRE_EVENT in self.config[DOMAIN] and \
self.config[DOMAIN][CONF_KNX_FIRE_EVENT]:
from xknx.knx import AddressFilter
address_filters = list(map(
AddressFilter,
self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER]))
self.xknx.telegram_queue.register_telegram_received_cb(
self.telegram_received_cb, address_filters)
@property
def cache(self):
"""Return the name given to the entity."""
return self._config.config.get('cache', True)
def group_write(self, value):
"""Write to the group address."""
KNXTUNNEL.group_write(self.address, [value])
def update(self):
"""Get the state from KNX bus or cache."""
from knxip.core import KNXException
try:
if self.state_address:
res = KNXTUNNEL.group_read(
self.state_address, use_cache=self.cache)
else:
res = KNXTUNNEL.group_read(self.address, use_cache=self.cache)
if res:
self._state = res[0]
self._data = res
else:
_LOGGER.debug(
"%s: unable to read from KNX address: %s (None)",
self.name, self.address
)
except KNXException:
_LOGGER.exception(
"%s: unable to read from KNX address: %s",
self.name, self.address
)
return False
class KNXMultiAddressDevice(Entity):
"""Representation of devices connected to a multiple KNX group address.
This is needed for devices like dimmers or shutter actuators as they have
to be controlled by multiple group addresses.
"""
def __init__(self, hass, config, required, optional=None):
"""Initialize the device.
The namelist argument lists the required addresses. E.g. for a dimming
actuators, the namelist might look like:
onoff_address: 0/0/1
brightness_address: 0/0/2
"""
from knxip.core import parse_group_address, KNXException
self.names = {}
self.values = {}
self._config = config
self._state = False
self._data = None
_LOGGER.debug(
"%s: initalizing KNX multi address device",
self.name
)
settings = self._config.config
if config.address:
_LOGGER.debug(
"%s: base address: address=%s",
self.name, settings.get('address')
)
self.names[config.address] = 'base'
if config.state_address:
_LOGGER.debug(
"%s, state address: state_address=%s",
self.name, settings.get('state_address')
)
self.names[config.state_address] = 'state'
# parse required addresses
for name in required:
paramname = '{}{}'.format(name, '_address')
addr = settings.get(paramname)
if addr is None:
_LOGGER.error(
"%s: Required KNX group address %s missing",
self.name, paramname
)
raise KNXException(
"%s: Group address for {} missing in "
"configuration for {}".format(
self.name, paramname
)
)
_LOGGER.debug(
"%s: (required parameter) %s=%s",
self.name, paramname, addr
)
addr = parse_group_address(addr)
self.names[addr] = name
# parse optional addresses
for name in optional:
paramname = '{}{}'.format(name, '_address')
addr = settings.get(paramname)
_LOGGER.debug(
"%s: (optional parameter) %s=%s",
self.name, paramname, addr
)
if addr:
try:
addr = parse_group_address(addr)
except KNXException:
_LOGGER.exception(
"%s: cannot parse group address %s",
self.name, addr
)
self.names[addr] = name
@property
def name(self):
"""Return the entity's display name."""
return self._config.name
@property
def config(self):
"""Return the entity's configuration."""
return self._config
@property
def should_poll(self):
"""Return the state of the polling, if needed."""
return self._config.should_poll
@property
def cache(self):
"""Return the name given to the entity."""
return self._config.config.get('cache', True)
def has_attribute(self, name):
"""Check if the attribute with the given name is defined.
This is mostly important for optional addresses.
"""
for attributename in self.names.values():
if attributename == name:
return True
@asyncio.coroutine
def telegram_received_cb(self, telegram):
"""Callback invoked after a KNX telegram was received."""
self.hass.bus.fire('knx_event', {
'address': telegram.group_address.str(),
'data': telegram.payload.value
})
# False signals XKNX to proceed with processing telegrams.
return False
def set_percentage(self, name, percentage):
"""Set a percentage in knx for a given attribute.
@asyncio.coroutine
def service_send_to_knx_bus(self, call):
"""Service for sending an arbitray KNX message to the KNX bus."""
from xknx.knx import Telegram, Address, DPTBinary, DPTArray
attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD)
attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS)
DPT_Scaling / DPT 5.001 is a single byte scaled percentage
"""
percentage = abs(percentage) # only accept positive values
scaled_value = percentage * 255 / 100
value = min(255, scaled_value)
return self.set_int_value(name, value)
def calculate_payload(attr_payload):
"""Calculate payload depending on type of attribute."""
if isinstance(attr_payload, int):
return DPTBinary(attr_payload)
return DPTArray(attr_payload)
payload = calculate_payload(attr_payload)
address = Address(attr_address)
def get_percentage(self, name):
"""Get a percentage from knx for a given attribute.
telegram = Telegram()
telegram.payload = payload
telegram.group_address = address
yield from self.xknx.telegrams.put(telegram)
DPT_Scaling / DPT 5.001 is a single byte scaled percentage
"""
value = self.get_int_value(name)
percentage = round(value * 100 / 255)
return percentage
def set_int_value(self, name, value, num_bytes=1):
"""Set an integer value for a given attribute."""
# KNX packets are big endian
value = round(value) # only accept integers
b_value = value.to_bytes(num_bytes, byteorder='big')
return self.set_value(name, list(b_value))
class KNXAutomation():
"""Wrapper around xknx.devices.ActionCallback object.."""
def get_int_value(self, name):
"""Get an integer value for a given attribute."""
# KNX packets are big endian
summed_value = 0
raw_value = self.value(name)
try:
# convert raw value in bytes
for val in raw_value:
summed_value *= 256
summed_value += val
except TypeError:
# pknx returns a non-iterable type for unsuccessful reads
pass
def __init__(self, hass, device, hook, action, counter=1):
"""Initialize Automation class."""
self.hass = hass
self.device = device
script_name = "{} turn ON script".format(device.get_name())
self.script = Script(hass, action, script_name)
return summed_value
def value(self, name):
"""Return the value to a given named attribute."""
from knxip.core import KNXException
addr = None
for attributeaddress, attributename in self.names.items():
if attributename == name:
addr = attributeaddress
if addr is None:
_LOGGER.error("%s: attribute '%s' undefined",
self.name, name)
_LOGGER.debug(
"%s: defined attributes: %s",
self.name, str(self.names)
)
return False
try:
res = KNXTUNNEL.group_read(addr, use_cache=self.cache)
except KNXException:
_LOGGER.exception(
"%s: unable to read from KNX address: %s",
self.name, addr
)
return False
return res
def set_value(self, name, value):
"""Set the value of a given named attribute."""
from knxip.core import KNXException
addr = None
for attributeaddress, attributename in self.names.items():
if attributename == name:
addr = attributeaddress
if addr is None:
_LOGGER.error("%s: attribute '%s' undefined",
self.name, name)
_LOGGER.debug(
"%s: defined attributes: %s",
self.name, str(self.names)
)
return False
try:
KNXTUNNEL.group_write(addr, value)
except KNXException:
_LOGGER.exception(
"%s: unable to write to KNX address: %s",
self.name, addr
)
return False
return True
import xknx
self.action = xknx.devices.ActionCallback(
hass.data[DATA_KNX].xknx,
self.script.async_run,
hook=hook,
counter=counter)
device.actions.append(self.action)
+1 -2
View File
@@ -24,8 +24,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
new_device = HMLight(hass, conf)
new_device.link_homematic()
new_device = HMLight(conf)
devices.append(new_device)
add_devices(devices)
+16
View File
@@ -83,6 +83,7 @@ SCENE_SCHEMA = vol.Schema({
})
ATTR_IS_HUE_GROUP = "is_hue_group"
GROUP_NAME_ALL_HUE_LIGHTS = "All Hue Lights"
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
@@ -203,6 +204,21 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
_LOGGER.error("Got unexpected result from Hue API")
return
if not skip_groups:
# Group ID 0 is a special group in the hub for all lights, but it
# is not returned by get_api() so explicity get it and include it.
# See https://developers.meethue.com/documentation/
# groups-api#21_get_all_groups
_LOGGER.debug("Getting group 0 from bridge")
all_lights = bridge.get_group(0)
if not isinstance(all_lights, dict):
_LOGGER.error("Got unexpected result from Hue API for group 0")
return
# Hue hub returns name of group 0 as "Group 0", so rename
# for ease of use in HA.
all_lights['name'] = GROUP_NAME_ALL_HUE_LIGHTS
api_groups["0"] = all_lights
new_lights = []
api_name = api.get('config').get('name')
+121 -71
View File
@@ -1,17 +1,17 @@
"""
Support KNX Lighting actuators.
Support for KNX/IP lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/Light.knx/
https://home-assistant.io/components/light.knx/
"""
import logging
import asyncio
import voluptuous as vol
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
from homeassistant.components.light import (Light, PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
ATTR_BRIGHTNESS)
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
from homeassistant.components.light import PLATFORM_SCHEMA, Light, \
SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
CONF_ADDRESS = 'address'
@@ -19,8 +19,6 @@ CONF_STATE_ADDRESS = 'state_address'
CONF_BRIGHTNESS_ADDRESS = 'brightness_address'
CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address'
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'KNX Light'
DEPENDENCIES = ['knx']
@@ -33,84 +31,136 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the KNX light platform."""
add_devices([KNXLight(hass, KNXConfig(config))])
@asyncio.coroutine
def async_setup_platform(hass, config, add_devices,
discovery_info=None):
"""Set up light(s) for KNX platform."""
if DATA_KNX not in hass.data \
or not hass.data[DATA_KNX].initialized:
return False
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, add_devices)
else:
async_add_devices_config(hass, config, add_devices)
return True
class KNXLight(KNXMultiAddressDevice, Light):
"""Representation of a KNX Light device."""
@callback
def async_add_devices_discovery(hass, discovery_info, add_devices):
"""Set up lights for KNX platform configured via xknx.yaml."""
entities = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name]
entities.append(KNXLight(hass, device))
add_devices(entities)
def __init__(self, hass, config):
"""Initialize the cover."""
KNXMultiAddressDevice.__init__(
self, hass, config,
[], # required
optional=['state', 'brightness', 'brightness_state']
)
self._hass = hass
self._supported_features = 0
if CONF_BRIGHTNESS_ADDRESS in config.config:
_LOGGER.debug("%s is dimmable", self.name)
self._supported_features = self._supported_features | \
SUPPORT_BRIGHTNESS
self._brightness = None
@callback
def async_add_devices_config(hass, config, add_devices):
"""Set up light for KNX platform configured within plattform."""
import xknx
light = xknx.devices.Light(
hass.data[DATA_KNX].xknx,
name=config.get(CONF_NAME),
group_address_switch=config.get(CONF_ADDRESS),
group_address_switch_state=config.get(CONF_STATE_ADDRESS),
group_address_brightness=config.get(CONF_BRIGHTNESS_ADDRESS),
group_address_brightness_state=config.get(
CONF_BRIGHTNESS_STATE_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(light)
add_devices([KNXLight(hass, light)])
def turn_on(self, **kwargs):
"""Turn the switch on.
This sends a value 1 to the group address of the device
"""
_LOGGER.debug("%s: turn on", self.name)
self.set_value('base', [1])
self._state = 1
class KNXLight(Light):
"""Representation of a KNX light."""
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
_LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
self.name, self._brightness)
assert self._brightness <= 255
self.set_value("brightness", [self._brightness])
def __init__(self, hass, device):
"""Initialization of KNXLight."""
self.device = device
self.hass = hass
self.async_register_callbacks()
if not self.should_poll:
self.schedule_update_ha_state()
@callback
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@asyncio.coroutine
def after_update_callback(device):
"""Callback after device was updated."""
# pylint: disable=unused-argument
yield from self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
def turn_off(self, **kwargs):
"""Turn the switch off.
@property
def name(self):
"""Return the name of the KNX device."""
return self.device.name
This sends a value 1 to the group address of the device
"""
_LOGGER.debug("%s: turn off", self.name)
self.set_value('base', [0])
self._state = 0
if not self.should_poll:
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed within KNX."""
return False
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self.device.brightness \
if self.device.supports_dimming else \
None
@property
def xy_color(self):
"""Return the XY color value [float, float]."""
return None
@property
def rgb_color(self):
"""Return the RBG color value."""
return None
@property
def color_temp(self):
"""Return the CT color temperature."""
return None
@property
def white_value(self):
"""Return the white value of this light between 0..255."""
return None
@property
def effect_list(self):
"""Return the list of supported effects."""
return None
@property
def effect(self):
"""Return the current effect."""
return None
@property
def is_on(self):
"""Return True if the value is not 0 is on, else False."""
return self._state != 0
"""Return true if light is on."""
return self.device.state
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
flags = 0
if self.device.supports_dimming:
flags |= SUPPORT_BRIGHTNESS
return flags
def update(self):
"""Update device state."""
super().update()
if self.has_attribute('brightness_state'):
value = self.value('brightness_state')
if value is not None:
self._brightness = int.from_bytes(value, byteorder='little')
_LOGGER.debug("%s: brightness = %d",
self.name, self._brightness)
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs and self.device.supports_dimming:
yield from self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS]))
else:
yield from self.device.set_on()
if self.has_attribute('state'):
self._state = self.value("state")[0]
_LOGGER.debug("%s: state = %d", self.name, self._state)
def should_poll(self):
"""No polling needed for a KNX light."""
return False
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the light off."""
yield from self.device.set_off()
@@ -7,7 +7,7 @@ https://home-assistant.io/components/light.lutron_caseta/
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, DOMAIN)
from homeassistant.components.light.lutron import (
to_hass_level, to_lutron_level)
from homeassistant.components.lutron_caseta import (
@@ -23,7 +23,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lutron Caseta lights."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
light_devices = bridge.get_devices_by_types(["WallDimmer", "PlugInDimmer"])
light_devices = bridge.get_devices_by_domain(DOMAIN)
for light_device in light_devices:
dev = LutronCasetaLight(light_device, bridge)
devs.append(dev)
+12 -2
View File
@@ -39,6 +39,7 @@ CONF_EFFECT_COMMAND_TOPIC = 'effect_command_topic'
CONF_EFFECT_LIST = 'effect_list'
CONF_EFFECT_STATE_TOPIC = 'effect_state_topic'
CONF_EFFECT_VALUE_TEMPLATE = 'effect_value_template'
CONF_RGB_COMMAND_TEMPLATE = 'rgb_command_template'
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
CONF_RGB_STATE_TOPIC = 'rgb_state_topic'
CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template'
@@ -75,6 +76,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_RGB_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
@@ -125,6 +127,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE),
CONF_EFFECT: config.get(CONF_EFFECT_VALUE_TEMPLATE),
CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE),
CONF_RGB_COMMAND_TEMPLATE: config.get(CONF_RGB_COMMAND_TEMPLATE),
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
CONF_WHITE_VALUE: config.get(CONF_WHITE_VALUE_TEMPLATE),
CONF_XY: config.get(CONF_XY_VALUE_TEMPLATE),
@@ -397,10 +400,17 @@ class MqttLight(Light):
if ATTR_RGB_COLOR in kwargs and \
self._topic[CONF_RGB_COMMAND_TOPIC] is not None:
tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE]
if tpl:
colors = {'red', 'green', 'blue'}
variables = {key: val for key, val in
zip(colors, kwargs[ATTR_RGB_COLOR])}
rgb_color_str = tpl.async_render(variables)
else:
rgb_color_str = '{},{},{}'.format(*kwargs[ATTR_RGB_COLOR])
mqtt.async_publish(
self.hass, self._topic[CONF_RGB_COMMAND_TOPIC],
'{},{},{}'.format(*kwargs[ATTR_RGB_COLOR]), self._qos,
self._retain)
rgb_color_str, self._qos, self._retain)
if self._optimistic_rgb:
self._rgb = kwargs[ATTR_RGB_COLOR]
+2 -2
View File
@@ -23,7 +23,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the RFXtrx platform."""
import RFXtrx as rfxtrxmod
lights = rfxtrx.get_devices_from_config(config, RfxtrxLight, hass)
lights = rfxtrx.get_devices_from_config(config, RfxtrxLight)
add_devices(lights)
def light_update(event):
@@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
not event.device.known_to_be_dimmable:
return
new_device = rfxtrx.get_new_device(event, config, RfxtrxLight, hass)
new_device = rfxtrx.get_new_device(event, config, RfxtrxLight)
if new_device:
add_devices([new_device])
+52 -47
View File
@@ -9,9 +9,10 @@ import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light)
from homeassistant.components.light import \
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS
from homeassistant.components.light import (
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA)
from homeassistant.components.tradfri import (
KEY_GATEWAY, KEY_TRADFRI_GROUPS, KEY_API)
from homeassistant.util import color as color_util
_LOGGER = logging.getLogger(__name__)
@@ -19,9 +20,7 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['tradfri']
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
IKEA = 'IKEA of Sweden'
ALLOWED_TEMPERATURES = {
IKEA: {2200: 'efd275', 2700: 'f1e0b5', 4000: 'f5faf6'}
}
ALLOWED_TEMPERATURES = {IKEA}
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -30,24 +29,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return
gateway_id = discovery_info['gateway']
api = hass.data[KEY_API][gateway_id]
gateway = hass.data[KEY_GATEWAY][gateway_id]
devices = gateway.get_devices()
lights = [dev for dev in devices if dev.has_light_control]
add_devices(Tradfri(light) for light in lights)
devices = api(gateway.get_devices())
lights = [dev for dev in devices if api(dev).has_light_control]
add_devices(Tradfri(light, api) for light in lights)
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
if allow_tradfri_groups:
groups = gateway.get_groups()
add_devices(TradfriGroup(group) for group in groups)
groups = api(gateway.get_groups())
add_devices(TradfriGroup(group, api) for group in groups)
class TradfriGroup(Light):
"""The platform class required by hass."""
def __init__(self, light):
def __init__(self, light, api):
"""Initialize a Group."""
self._group = light
self._name = light.name
self._group = api(light)
self._api = api
self._name = self._group.name
@property
def supported_features(self):
@@ -71,20 +72,20 @@ class TradfriGroup(Light):
def turn_off(self, **kwargs):
"""Instruct the group lights to turn off."""
self._group.set_state(0)
self._api(self._group.set_state(0))
def turn_on(self, **kwargs):
"""Instruct the group lights to turn on, or dim."""
if ATTR_BRIGHTNESS in kwargs:
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS])
self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS]))
else:
self._group.set_state(1)
self._api(self._group.set_state(1))
def update(self):
"""Fetch new state data for this group."""
from pytradfri import RequestTimeout
try:
self._group.update()
self._api(self._group.update())
except RequestTimeout:
_LOGGER.warning("Tradfri update request timed out")
@@ -92,14 +93,15 @@ class TradfriGroup(Light):
class Tradfri(Light):
"""The platform class required by Home Asisstant."""
def __init__(self, light):
def __init__(self, light, api):
"""Initialize a Light."""
self._light = light
self._light = api(light)
self._api = api
# Caching of LightControl and light object
self._light_control = light.light_control
self._light_data = light.light_control.lights[0]
self._name = light.name
self._light_control = self._light.light_control
self._light_data = self._light_control.lights[0]
self._name = self._light.name
self._rgb_color = None
self._features = SUPPORT_BRIGHTNESS
@@ -109,8 +111,20 @@ class Tradfri(Light):
else:
self._features |= SUPPORT_RGB_COLOR
self._ok_temps = ALLOWED_TEMPERATURES.get(
self._light.device_info.manufacturer)
self._ok_temps = \
self._light.device_info.manufacturer in ALLOWED_TEMPERATURES
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
from pytradfri.color import MAX_KELVIN_WS
return color_util.color_temperature_kelvin_to_mired(MAX_KELVIN_WS)
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
from pytradfri.color import MIN_KELVIN_WS
return color_util.color_temperature_kelvin_to_mired(MIN_KELVIN_WS)
@property
def supported_features(self):
@@ -135,20 +149,13 @@ class Tradfri(Light):
@property
def color_temp(self):
"""Return the CT color value in mireds."""
if (self._light_data.hex_color is None or
if (self._light_data.kelvin_color is None or
self.supported_features & SUPPORT_COLOR_TEMP == 0 or
not self._ok_temps):
return None
kelvin = next((
kelvin for kelvin, hex_color in self._ok_temps.items()
if hex_color == self._light_data.hex_color), None)
if kelvin is None:
_LOGGER.error(
"Unexpected color temperature found for %s: %s",
self.name, self._light_data.hex_color)
return
return color_util.color_temperature_kelvin_to_mired(kelvin)
return color_util.color_temperature_kelvin_to_mired(
self._light_data.kelvin_color
)
@property
def rgb_color(self):
@@ -157,7 +164,7 @@ class Tradfri(Light):
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
self._light_control.set_state(False)
self._api(self._light_control.set_state(False))
def turn_on(self, **kwargs):
"""
@@ -167,29 +174,27 @@ class Tradfri(Light):
for ATTR_RGB_COLOR, this also supports Philips Hue bulbs.
"""
if ATTR_BRIGHTNESS in kwargs:
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS])
self._api(self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS]))
else:
self._light_control.set_state(True)
self._api(self._light_control.set_state(True))
if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None:
self._light.light_control.set_hex_color(
color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR]))
self._api(self._light.light_control.set_hex_color(
color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR])))
elif ATTR_COLOR_TEMP in kwargs and \
self._light_data.hex_color is not None and self._ok_temps:
kelvin = color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP])
# find closest allowed kelvin temp from user input
kelvin = min(self._ok_temps.keys(), key=lambda x: abs(x - kelvin))
self._light_control.set_hex_color(self._ok_temps[kelvin])
self._api(self._light_control.set_kelvin_color(kelvin))
def update(self):
"""Fetch new state data for this light."""
from pytradfri import RequestTimeout
try:
self._light.update()
except RequestTimeout:
_LOGGER.warning("Tradfri update request timed out")
self._api(self._light.update())
except RequestTimeout as exception:
_LOGGER.warning("Tradfri update request timed out: %s", exception)
# Handle Hue lights paired with the gateway
# hex_color is 0 when bulb is unreachable
@@ -0,0 +1,227 @@
"""
Support for Xiaomi Philips Lights (LED Ball & Ceil).
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/light.xiaomi_philipslight/
"""
import asyncio
from functools import partial
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.light import (
PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS,
ATTR_COLOR_TEMP, SUPPORT_COLOR_TEMP, Light, )
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, )
from homeassistant.exceptions import PlatformNotReady
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Philips Light'
PLATFORM = 'xiaomi_philipslight'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
REQUIREMENTS = ['python-mirobo==0.1.3']
# The light does not accept cct values < 1
CCT_MIN = 1
CCT_MAX = 100
SUCCESS = ['ok']
ATTR_MODEL = 'model'
# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the light from config."""
from mirobo import Ceil, DeviceException
if PLATFORM not in hass.data:
hass.data[PLATFORM] = {}
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
try:
light = Ceil(host, token)
device_info = light.info()
_LOGGER.info("%s %s %s initialized",
device_info.raw['model'],
device_info.raw['fw_ver'],
device_info.raw['hw_ver'])
philips_light = XiaomiPhilipsLight(name, light, device_info)
hass.data[PLATFORM][host] = philips_light
except DeviceException:
raise PlatformNotReady
async_add_devices([philips_light], update_before_add=True)
class XiaomiPhilipsLight(Light):
"""Representation of a Xiaomi Philips Light."""
def __init__(self, name, light, device_info):
"""Initialize the light device."""
self._name = name
self._device_info = device_info
self._brightness = None
self._color_temp = None
self._light = light
self._state = None
self._state_attrs = {
ATTR_MODEL: self._device_info.raw['model'],
}
@property
def should_poll(self):
"""Poll the light."""
return True
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def available(self):
"""Return true when state is known."""
return self._state is not None
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
return self._state_attrs
@property
def is_on(self):
"""Return true if light is on."""
return self._state
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
def color_temp(self):
"""Return the color temperature."""
return self._color_temp
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return 175
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return 333
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
@asyncio.coroutine
def _try_command(self, mask_error, func, *args, **kwargs):
"""Call a light command handling error messages."""
from mirobo import DeviceException
try:
result = yield from self.hass.async_add_job(
partial(func, *args, **kwargs))
_LOGGER.debug("Response received from light: %s", result)
return result == SUCCESS
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
return False
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
percent_brightness = int(100 * brightness / 255)
_LOGGER.debug(
"Setting brightness: %s %s%%",
self.brightness, percent_brightness)
result = yield from self._try_command(
"Setting brightness failed: %s",
self._light.set_bright, percent_brightness)
if result:
self._brightness = brightness
if ATTR_COLOR_TEMP in kwargs:
color_temp = kwargs[ATTR_COLOR_TEMP]
percent_color_temp = self.translate(
color_temp, self.max_mireds,
self.min_mireds, CCT_MIN, CCT_MAX)
_LOGGER.debug(
"Setting color temperature: "
"%s mireds, %s%% cct",
color_temp, percent_color_temp)
result = yield from self._try_command(
"Setting color temperature failed: %s cct",
self._light.set_cct, percent_color_temp)
if result:
self._color_temp = color_temp
result = yield from self._try_command(
"Turning the light on failed.", self._light.on)
if result:
self._state = True
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the light off."""
result = yield from self._try_command(
"Turning the light off failed.", self._light.off)
if result:
self._state = True
@asyncio.coroutine
def async_update(self):
"""Fetch state from the device."""
from mirobo import DeviceException
try:
state = yield from self.hass.async_add_job(self._light.status)
_LOGGER.debug("Got new state: %s", state.data)
self._state = state.is_on
self._brightness = int(255 * 0.01 * state.bright)
self._color_temp = self.translate(state.cct, CCT_MIN, CCT_MAX,
self.max_mireds,
self.min_mireds)
except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)
@staticmethod
def translate(value, left_min, left_max, right_min, right_max):
"""Map a value from left span to right span."""
left_span = left_max - left_min
right_span = right_max - right_min
value_scaled = float(value - left_min) / float(left_span)
return int(right_min + (value_scaled * right_span))
+7 -7
View File
@@ -27,8 +27,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
endpoint = discovery_info['endpoint']
try:
primaries = yield from endpoint.light_color['num_primaries']
discovery_info['num_primaries'] = primaries
discovery_info['color_capabilities'] \
= yield from endpoint.light_color['color_capabilities']
except (AttributeError, KeyError):
pass
@@ -54,11 +54,11 @@ class Light(zha.Entity, light.Light):
self._supported_features |= light.SUPPORT_TRANSITION
self._brightness = 0
if zcl_clusters.lighting.Color.cluster_id in self._in_clusters:
# Not sure all color lights necessarily support this directly
# Should we emulate it?
self._supported_features |= light.SUPPORT_COLOR_TEMP
# Silly heuristic, not sure if it works widely
if kwargs.get('num_primaries', 1) >= 3:
color_capabilities = kwargs.get('color_capabilities', 0x10)
if color_capabilities & 0x10:
self._supported_features |= light.SUPPORT_COLOR_TEMP
if color_capabilities & 0x08:
self._supported_features |= light.SUPPORT_XY_COLOR
self._supported_features |= light.SUPPORT_RGB_COLOR
self._xy_color = (1.0, 1.0)
+49
View File
@@ -0,0 +1,49 @@
"""
This component provides HA lock support for Abode Security System.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.abode/
"""
import logging
from homeassistant.components.abode import AbodeDevice, DATA_ABODE
from homeassistant.components.lock import LockDevice
DEPENDENCIES = ['abode']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Abode lock devices."""
import abodepy.helpers.constants as CONST
abode = hass.data[DATA_ABODE]
sensors = []
for sensor in abode.get_devices(type_filter=(CONST.DEVICE_DOOR_LOCK)):
sensors.append(AbodeLock(abode, sensor))
add_devices(sensors)
class AbodeLock(AbodeDevice, LockDevice):
"""Representation of an Abode lock."""
def __init__(self, controller, device):
"""Initialize the Abode device."""
AbodeDevice.__init__(self, controller, device)
def lock(self, **kwargs):
"""Lock the device."""
self._device.lock()
def unlock(self, **kwargs):
"""Unlock the device."""
self._device.unlock()
@property
def is_locked(self):
"""Return true if device is on."""
return self._device.is_locked
+1 -1
View File
@@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.lock import (LockDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME)
REQUIREMENTS = ['pynello==1.5']
REQUIREMENTS = ['pynello==1.5.1']
_LOGGER = logging.getLogger(__name__)
+57
View File
@@ -0,0 +1,57 @@
"""
Support for Tesla door locks.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.tesla/
"""
import logging
from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['tesla']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tesla lock platform."""
devices = [TeslaLock(device, hass.data[TESLA_DOMAIN]['controller'])
for device in hass.data[TESLA_DOMAIN]['devices']['lock']]
add_devices(devices, True)
class TeslaLock(TeslaDevice, LockDevice):
"""Representation of a Tesla door lock."""
def __init__(self, tesla_device, controller):
"""Initialisation of the lock."""
self._state = None
super().__init__(tesla_device, controller)
self._name = self.tesla_device.name
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
def lock(self, **kwargs):
"""Send the lock command."""
_LOGGER.debug("Locking doors for: %s", self._name)
self.tesla_device.lock()
self._state = STATE_LOCKED
def unlock(self, **kwargs):
"""Send the unlock command."""
_LOGGER.debug("Unlocking doors for: %s", self._name)
self.tesla_device.unlock()
self._state = STATE_UNLOCKED
@property
def is_locked(self):
"""Get whether the lock is in locked state."""
return self._state == STATE_LOCKED
def update(self):
"""Updating state of the lock."""
_LOGGER.debug("Updating state for: %s", self._name)
self.tesla_device.update()
self._state = STATE_LOCKED if self.tesla_device.is_locked() \
else STATE_UNLOCKED
+1 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pylutron-caseta==0.2.7']
REQUIREMENTS = ['pylutron-caseta==0.2.8']
_LOGGER = logging.getLogger(__name__)
+1 -1
View File
@@ -15,7 +15,7 @@ from homeassistant.components.media_player import (
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2017.8.18']
REQUIREMENTS = ['youtube_dl==2017.9.2']
_LOGGER = logging.getLogger(__name__)
@@ -17,15 +17,16 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY)
from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
CONF_NAME, STATE_ON, CONF_ZONE)
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.5.2']
REQUIREMENTS = ['denonavr==0.5.3']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = None
DEFAULT_SHOW_SOURCES = False
DEFAULT_TIMEOUT = 2
CONF_SHOW_ALL_SOURCES = 'show_all_sources'
CONF_ZONES = 'zones'
CONF_VALID_ZONES = ['Zone2', 'Zone3']
@@ -51,7 +52,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES):
cv.boolean,
vol.Optional(CONF_ZONES):
vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA])
vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA]),
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
})
NewHost = namedtuple('NewHost', ['host', 'name'])
@@ -69,8 +71,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if cache is None:
cache = hass.data[KEY_DENON_CACHE] = set()
# Get config option for show_all_sources
# Get config option for show_all_sources and timeout
show_all_sources = config.get(CONF_SHOW_ALL_SOURCES)
timeout = config.get(CONF_TIMEOUT)
# Get config option for additional zones
zones = config.get(CONF_ZONES)
@@ -103,14 +106,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for d_receiver in d_receivers:
host = d_receiver["host"]
name = d_receiver["friendlyName"]
new_hosts.append(NewHost(host=host, name=name))
new_hosts.append(
NewHost(host=host, name=name))
for entry in new_hosts:
# Check if host not in cache, append it and save for later
# starting
if entry.host not in cache:
new_device = denonavr.DenonAVR(
entry.host, entry.name, show_all_sources, add_zones)
host=entry.host, name=entry.name,
show_all_inputs=show_all_sources, timeout=timeout,
add_zones=add_zones)
for new_zone in new_device.zones.values():
receivers.append(DenonDevice(new_zone))
cache.add(host)
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.directv/
"""
import voluptuous as vol
import requests
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
@@ -25,7 +26,7 @@ SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY
KNOWN_HOSTS = []
DATA_DIRECTV = "data_directv"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
@@ -37,32 +38,45 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the DirecTV platform."""
known_devices = hass.data.get(DATA_DIRECTV)
if not known_devices:
known_devices = []
hosts = []
if discovery_info:
host = discovery_info.get('host')
if host in KNOWN_HOSTS:
return
hosts.append([
'DirecTV_' + discovery_info.get('serial', ''),
host, DEFAULT_PORT
])
elif CONF_HOST in config:
if CONF_HOST in config:
hosts.append([
config.get(CONF_NAME), config.get(CONF_HOST),
config.get(CONF_PORT), config.get(CONF_DEVICE)
])
elif discovery_info:
host = discovery_info.get('host')
name = 'DirecTV_' + discovery_info.get('serial', '')
# attempt to discover additional RVU units
try:
resp = requests.get(
'http://%s:%d/info/getLocations' % (host, DEFAULT_PORT)).json()
if "locations" in resp:
for loc in resp["locations"]:
if("locationName" in loc and "clientAddr" in loc
and loc["clientAddr"] not in known_devices):
hosts.append([str.title(loc["locationName"]), host,
DEFAULT_PORT, loc["clientAddr"]])
except requests.exceptions.RequestException:
# bail out and just go forward with uPnP data
if DEFAULT_DEVICE not in known_devices:
hosts.append([name, host, DEFAULT_PORT, DEFAULT_DEVICE])
dtvs = []
for host in hosts:
dtvs.append(DirecTvDevice(*host))
KNOWN_HOSTS.append(host)
known_devices.append(host[-1])
add_devices(dtvs)
hass.data[DATA_DIRECTV] = known_devices
return True
+11 -4
View File
@@ -322,6 +322,7 @@ class SonosDevice(MediaPlayerDevice):
self._media_title = None
self._media_radio_show = None
self._media_next_title = None
self._available = True
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
@@ -386,6 +387,11 @@ class SonosDevice(MediaPlayerDevice):
"""Return coordinator of this player."""
return self._coordinator
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
def _is_available(self):
try:
sock = socket.create_connection(
@@ -416,11 +422,11 @@ class SonosDevice(MediaPlayerDevice):
self._player.get_sonos_favorites()['favorites']
if self._last_avtransport_event:
is_available = True
self._available = True
else:
is_available = self._is_available()
self._available = self._is_available()
if not is_available:
if not self._available:
self._player_volume = None
self._player_volume_muted = None
self._status = 'OFF'
@@ -897,7 +903,8 @@ class SonosDevice(MediaPlayerDevice):
src = fav.pop()
self._source_name = src['title']
if 'object.container.playlistContainer' in src['meta']:
if ('object.container.playlistContainer' in src['meta'] or
'object.container.album.musicAlbum' in src['meta']):
self._replace_queue_with_playlist(src)
self._player.play_from_queue(0)
else:
@@ -148,6 +148,10 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
new_token = \
self._oauth.refresh_access_token(
self._token_info['refresh_token'])
# skip when refresh failed
if new_token is None:
return
self._token_info = new_token
token_refreshed = True
if self._player is None or token_refreshed:
@@ -158,6 +162,12 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
def update(self):
"""Update state and attributes."""
self.refresh_spotify_instance()
# Don't true update when token is expired
if self._oauth.is_token_expired(self._token_info):
_LOGGER.warning("Spotify failed to update, token expired.")
return
# Available devices
player_devices = self._player.devices()
if player_devices is not None:
@@ -0,0 +1,233 @@
"""Example for configuration.yaml.
media_player:
- platform: yamaha_musiccast
name: "Living Room"
host: 192.168.xxx.xx
port: 5005
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT,
STATE_UNKNOWN, STATE_ON
)
from homeassistant.components.media_player import (
MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
SUPPORT_SELECT_SOURCE, SUPPORT_STOP
)
_LOGGER = logging.getLogger(__name__)
SUPPORTED_FEATURES = (
SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP |
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |
SUPPORT_TURN_ON | SUPPORT_TURN_OFF |
SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |
SUPPORT_SELECT_SOURCE
)
REQUIREMENTS = ['pymusiccast==0.1.0']
DEFAULT_NAME = "Yamaha Receiver"
DEFAULT_PORT = 5005
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.positive_int,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Yamaha MusicCast platform."""
import pymusiccast
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
receiver = pymusiccast.McDevice(host, udp_port=port)
_LOGGER.debug("receiver: %s / Port: %d", receiver, port)
add_devices([YamahaDevice(receiver, name)], True)
class YamahaDevice(MediaPlayerDevice):
"""Representation of a Yamaha MusicCast device."""
def __init__(self, receiver, name):
"""Initialize the Yamaha MusicCast device."""
self._receiver = receiver
self._name = name
self.power = STATE_UNKNOWN
self.volume = 0
self.volume_max = 0
self.mute = False
self._source = None
self._source_list = []
self.status = STATE_UNKNOWN
self.media_status = None
self._receiver.set_yamaha_device(self)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
if self.power == STATE_ON and self.status is not STATE_UNKNOWN:
return self.status
return self.power
@property
def should_poll(self):
"""Push an update after each command."""
return True
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self.mute
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self.volume
@property
def supported_features(self):
"""Flag of features that are supported."""
return SUPPORTED_FEATURES
@property
def source(self):
"""Return the current input source."""
return self._source
@property
def source_list(self):
"""List of available input sources."""
return self._source_list
@source_list.setter
def source_list(self, value):
"""Set source_list attribute."""
self._source_list = value
@property
def media_content_type(self):
"""Return the media content type."""
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self.media_status.media_duration \
if self.media_status else None
@property
def media_image_url(self):
"""Image url of current playing media."""
return self.media_status.media_image_url \
if self.media_status else None
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self.media_status.media_artist if self.media_status else None
@property
def media_album(self):
"""Album of current playing media, music track only."""
return self.media_status.media_album if self.media_status else None
@property
def media_track(self):
"""Track number of current playing media, music track only."""
return self.media_status.media_track if self.media_status else None
@property
def media_title(self):
"""Title of current playing media."""
return self.media_status.media_title if self.media_status else None
def update(self):
"""Get the latest details from the device."""
_LOGGER.debug("update: %s", self.entity_id)
# call from constructor setup_platform()
if not self.entity_id:
_LOGGER.debug("First run")
self._receiver.update_status(push=False)
# call from regular polling
else:
# update_status_timer was set before
if self._receiver.update_status_timer:
_LOGGER.debug(
"is_alive: %s",
self._receiver.update_status_timer.is_alive())
# e.g. computer was suspended, while hass was running
if not self._receiver.update_status_timer.is_alive():
_LOGGER.debug("Reinitializing")
self._receiver.update_status()
def turn_on(self):
"""Turn on specified media player or all."""
_LOGGER.debug("Turn device: on")
self._receiver.set_power(True)
def turn_off(self):
"""Turn off specified media player or all."""
_LOGGER.debug("Turn device: off")
self._receiver.set_power(False)
def media_play(self):
"""Send the media player the command for play/pause."""
_LOGGER.debug("Play")
self._receiver.set_playback("play")
def media_pause(self):
"""Send the media player the command for pause."""
_LOGGER.debug("Pause")
self._receiver.set_playback("pause")
def media_stop(self):
"""Send the media player the stop command."""
_LOGGER.debug("Stop")
self._receiver.set_playback("stop")
def media_previous_track(self):
"""Send the media player the command for prev track."""
_LOGGER.debug("Previous")
self._receiver.set_playback("previous")
def media_next_track(self):
"""Send the media player the command for next track."""
_LOGGER.debug("Next")
self._receiver.set_playback("next")
def mute_volume(self, mute):
"""Send mute command."""
_LOGGER.debug("Mute volume: %s", mute)
self._receiver.set_mute(mute)
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
_LOGGER.debug("Volume level: %.2f / %d",
volume, volume * self.volume_max)
self._receiver.set_volume(volume * self.volume_max)
def select_source(self, source):
"""Send the media player the command to select input source."""
_LOGGER.debug("select_source: %s", source)
self.status = STATE_UNKNOWN
self._receiver.set_input(source)
+35
View File
@@ -0,0 +1,35 @@
"""
Support for Mycroft AI.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mycroft
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['mycroftapi==2.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'mycroft'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Mycroft component."""
hass.data[DOMAIN] = config[DOMAIN][CONF_HOST]
discovery.load_platform(hass, 'notify', DOMAIN, {}, config)
return True
+25 -7
View File
@@ -27,7 +27,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
from homeassistant.setup import setup_component
REQUIREMENTS = ['pymysensors==0.11.0']
REQUIREMENTS = ['pymysensors==0.11.1']
_LOGGER = logging.getLogger(__name__)
@@ -49,6 +49,9 @@ CONF_TOPIC_IN_PREFIX = 'topic_in_prefix'
CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix'
CONF_VERSION = 'version'
CONF_NODES = 'nodes'
CONF_NODE_NAME = 'name'
DEFAULT_BAUD_RATE = 115200
DEFAULT_TCP_PORT = 5003
DEFAULT_VERSION = '1.4'
@@ -132,6 +135,12 @@ def deprecated(key):
return validator
NODE_SCHEMA = vol.Schema({
cv.positive_int: {
vol.Required(CONF_NODE_NAME): cv.string
}
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), {
vol.Required(CONF_GATEWAYS): vol.All(
@@ -151,6 +160,7 @@ CONFIG_SCHEMA = vol.Schema({
CONF_TOPIC_IN_PREFIX, default=''): valid_subscribe_topic,
vol.Optional(
CONF_TOPIC_OUT_PREFIX, default=''): valid_publish_topic,
vol.Optional(CONF_NODES, default={}): NODE_SCHEMA,
}]
),
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
@@ -358,6 +368,7 @@ def setup(hass, config):
device, persistence_file, baud_rate, tcp_port, in_prefix,
out_prefix)
if ready_gateway is not None:
ready_gateway.nodes_config = gway.get(CONF_NODES)
gateways[id(ready_gateway)] = ready_gateway
if not gateways:
@@ -474,12 +485,14 @@ def gw_callback_factory(hass):
validated = validate_child(msg.gateway, msg.node_id, child)
for platform, dev_ids in validated.items():
devices = get_mysensors_devices(hass, platform)
for idx, dev_id in enumerate(list(dev_ids)):
new_dev_ids = []
for dev_id in dev_ids:
if dev_id in devices:
dev_ids.pop(idx)
signals.append(SIGNAL_CALLBACK.format(*dev_id))
if dev_ids:
discover_mysensors_platform(hass, platform, dev_ids)
else:
new_dev_ids.append(dev_id)
if new_dev_ids:
discover_mysensors_platform(hass, platform, new_dev_ids)
for signal in set(signals):
# Only one signal per device is needed.
# A device can have multiple platforms, ie multiple schemas.
@@ -495,8 +508,13 @@ def gw_callback_factory(hass):
def get_mysensors_name(gateway, node_id, child_id):
"""Return a name for a node child."""
return '{} {} {}'.format(
gateway.sensors[node_id].sketch_name, node_id, child_id)
node_name = '{} {}'.format(
gateway.sensors[node_id].sketch_name, node_id)
node_name = next(
(node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items()
if node.get(CONF_NODE_NAME) is not None and conf_id == node_id),
node_name)
return '{} {}'.format(node_name, child_id)
def get_mysensors_gateway(hass, gateway_id):
+9 -4
View File
@@ -82,8 +82,6 @@ def async_setup(hass, config):
"""Set up a notify platform."""
if p_config is None:
p_config = {}
if discovery_info is None:
discovery_info = {}
platform = yield from async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
@@ -105,8 +103,12 @@ def async_setup(hass, config):
raise HomeAssistantError("Invalid notify platform.")
if notify_service is None:
_LOGGER.error(
"Failed to initialize notification service %s", p_type)
# Platforms can decide not to create a service based
# on discovery data.
if discovery_info is None:
_LOGGER.error(
"Failed to initialize notification service %s",
p_type)
return
except Exception: # pylint: disable=broad-except
@@ -115,6 +117,9 @@ def async_setup(hass, config):
notify_service.hass = hass
if discovery_info is None:
discovery_info = {}
@asyncio.coroutine
def async_notify_message(service):
"""Handle sending notification message service calls."""
+1 -1
View File
@@ -15,7 +15,7 @@ from homeassistant.components.notify import (
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['discord.py==0.16.10']
REQUIREMENTS = ['discord.py==0.16.11']
CONF_TOKEN = 'token'
+99
View File
@@ -0,0 +1,99 @@
"""
KNX/IP notification service.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/notify.knx/
"""
import asyncio
import voluptuous as vol
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
from homeassistant.components.notify import PLATFORM_SCHEMA, \
BaseNotificationService
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
CONF_ADDRESS = 'address'
DEFAULT_NAME = 'KNX Notify'
DEPENDENCIES = ['knx']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
@asyncio.coroutine
def async_get_service(hass, config, discovery_info=None):
"""Get the KNX notification service."""
if DATA_KNX not in hass.data \
or not hass.data[DATA_KNX].initialized:
return False
return async_get_service_discovery(hass, discovery_info) \
if discovery_info is not None else \
async_get_service_config(hass, config)
@callback
def async_get_service_discovery(hass, discovery_info):
"""Set up notifications for KNX platform configured via xknx.yaml."""
notification_devices = []
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
device = hass.data[DATA_KNX].xknx.devices[device_name]
notification_devices.append(device)
return \
KNXNotificationService(hass, notification_devices) \
if notification_devices else \
None
@callback
def async_get_service_config(hass, config):
"""Set up notification for KNX platform configured within plattform."""
import xknx
notification = xknx.devices.Notification(
hass.data[DATA_KNX].xknx,
name=config.get(CONF_NAME),
group_address=config.get(CONF_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(notification)
return KNXNotificationService(hass, [notification, ])
class KNXNotificationService(BaseNotificationService):
"""Implement demo notification service."""
def __init__(self, hass, devices):
"""Initialize the service."""
self.hass = hass
self.devices = devices
@property
def targets(self):
"""Return a dictionary of registered targets."""
ret = {}
for device in self.devices:
ret[device.name] = device.name
return ret
@asyncio.coroutine
def async_send_message(self, message="", **kwargs):
"""Send a notification to knx bus."""
if "target" in kwargs:
yield from self._async_send_to_device(message, kwargs["target"])
else:
yield from self._async_send_to_all_devices(message)
@asyncio.coroutine
def _async_send_to_all_devices(self, message):
"""Send a notification to knx bus to all connected devices."""
for device in self.devices:
yield from device.set(message)
@asyncio.coroutine
def _async_send_to_device(self, message, names):
"""Send a notification to knx bus to device with given names."""
for device in self.devices:
if device.name in names:
yield from device.set(message)
@@ -0,0 +1,40 @@
"""
Mycroft AI notification platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.mycroft/
"""
import logging
from homeassistant.components.notify import BaseNotificationService
DEPENDENCIES = ['mycroft']
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config, discovery_info=None):
"""Get the Mycroft notification service."""
return MycroftNotificationService(
hass.data['mycroft'])
class MycroftNotificationService(BaseNotificationService):
"""The Mycroft Notification Service."""
def __init__(self, mycroft_ip):
"""Initialize the service."""
self.mycroft_ip = mycroft_ip
def send_message(self, message="", **kwargs):
"""Send a message mycroft to speak on instance."""
from mycroftapi import MycroftAPI
text = message
mycroft = MycroftAPI(self.mycroft_ip)
if mycroft is not None:
mycroft.speak_text(text)
else:
_LOGGER.log("Could not reach this instance of mycroft")
+43 -26
View File
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.pushbullet/
"""
import logging
import mimetypes
import voluptuous as vol
@@ -20,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_URL = 'url'
ATTR_FILE = 'file'
ATTR_FILE_URL = 'file_url'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
@@ -80,16 +82,11 @@ class PushBulletNotificationService(BaseNotificationService):
targets = kwargs.get(ATTR_TARGET)
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = kwargs.get(ATTR_DATA)
url = None
filepath = None
if data:
url = data.get(ATTR_URL, None)
filepath = data.get(ATTR_FILE, None)
refreshed = False
if not targets:
# Backward compatibility, notify all devices in own account
self._push_data(filepath, message, title, url)
self._push_data(message, title, data, self.pushbullet)
_LOGGER.info("Sent notification to self")
return
@@ -104,7 +101,7 @@ class PushBulletNotificationService(BaseNotificationService):
# Target is email, send directly, don't use a target object
# This also seems works to send to all devices in own account
if ttype == 'email':
self._push_data(filepath, message, title, url, tname)
self._push_data(message, title, data, self.pushbullet, tname)
_LOGGER.info("Sent notification to email %s", tname)
continue
@@ -123,27 +120,47 @@ class PushBulletNotificationService(BaseNotificationService):
# Attempt push_note on a dict value. Keys are types & target
# name. Dict pbtargets has all *actual* targets.
try:
if url:
self.pbtargets[ttype][tname].push_link(
title, url, body=message)
else:
self.pbtargets[ttype][tname].push_note(title, message)
self._push_data(message, title, data,
self.pbtargets[ttype][tname])
_LOGGER.info("Sent notification to %s/%s", ttype, tname)
except KeyError:
_LOGGER.error("No such target: %s/%s", ttype, tname)
continue
except self.pushbullet.errors.PushError:
_LOGGER.error("Notify failed to: %s/%s", ttype, tname)
continue
def _push_data(self, filepath, message, title, url, tname=None):
if url:
self.pushbullet.push_link(
title, url, body=message, email=tname)
elif filepath and self.hass.config.is_allowed_path(filepath):
with open(filepath, "rb") as fileh:
filedata = self.pushbullet.upload_file(fileh, filepath)
self.pushbullet.push_file(title=title, body=message,
**filedata)
else:
self.pushbullet.push_note(title, message, email=tname)
def _push_data(self, message, title, data, pusher, tname=None):
from pushbullet import PushError
if data is None:
data = {}
url = data.get(ATTR_URL)
filepath = data.get(ATTR_FILE)
file_url = data.get(ATTR_FILE_URL)
try:
if url:
if tname:
pusher.push_link(title, url, body=message, email=tname)
else:
pusher.push_link(title, url, body=message)
elif filepath:
if not self.hass.config.is_allowed_path(filepath):
_LOGGER.error("Filepath is not valid or allowed.")
return
with open(filepath, "rb") as fileh:
filedata = self.pushbullet.upload_file(fileh, filepath)
if filedata.get('file_type') == 'application/x-empty':
_LOGGER.error("Can not send an empty file.")
return
pusher.push_file(title=title, body=message, **filedata)
elif file_url:
if not file_url.startswith('http'):
_LOGGER.error("Url should start with http or https.")
return
pusher.push_file(title=title, body=message, file_name=file_url,
file_url=file_url,
file_type=mimetypes.guess_type(file_url)[0])
else:
if tname:
pusher.push_note(title, message, email=tname)
else:
pusher.push_note(title, message)
except PushError as err:
_LOGGER.error("Notify failed: %s", err)
+1 -1
View File
@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['sendgrid==5.0.0']
REQUIREMENTS = ['sendgrid==5.2.0']
_LOGGER = logging.getLogger(__name__)
+88 -26
View File
@@ -8,6 +8,8 @@ import json
import logging
import mimetypes
import os
from datetime import timedelta, datetime
from functools import partial
import voluptuous as vol
@@ -15,6 +17,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
from homeassistant.helpers.event import async_track_point_in_time
REQUIREMENTS = ['TwitterAPI==2.4.6']
@@ -68,49 +71,70 @@ class TwitterNotificationService(BaseNotificationService):
_LOGGER.warning("'%s' is not a whitelisted directory", media)
return
media_id = self.upload_media(media)
callback = partial(self.send_message_callback, message)
self.upload_media_then_callback(callback, media)
def send_message_callback(self, message, media_id):
"""Tweet a message, optionally with media."""
if self.user:
resp = self.api.request('direct_messages/new',
{'text': message, 'user': self.user,
{'user': self.user,
'text': message,
'media_ids': media_id})
else:
resp = self.api.request('statuses/update',
{'status': message, 'media_ids': media_id})
{'status': message,
'media_ids': media_id})
if resp.status_code != 200:
self.log_error_resp(resp)
else:
_LOGGER.debug("Message posted: %s", resp.json())
def upload_media(self, media_path=None):
def upload_media_then_callback(self, callback, media_path=None):
"""Upload media."""
if not media_path:
return None
with open(media_path, 'rb') as file:
total_bytes = os.path.getsize(media_path)
(media_category, media_type) = self.media_info(media_path)
resp = self.upload_media_init(
media_type, media_category, total_bytes
)
if 199 > resp.status_code < 300:
self.log_error_resp(resp)
return None
media_id = resp.json()['media_id']
media_id = self.upload_media_chunked(file, total_bytes, media_id)
resp = self.upload_media_finalize(media_id)
if 199 > resp.status_code < 300:
self.log_error_resp(resp)
return None
if resp.json().get('processing_info') is None:
return callback(media_id)
self.check_status_until_done(media_id, callback)
def media_info(self, media_path):
"""Determine mime type and Twitter media category for given media."""
(media_type, _) = mimetypes.guess_type(media_path)
total_bytes = os.path.getsize(media_path)
media_category = self.media_category_for_type(media_type)
_LOGGER.debug("media %s is mime type %s and translates to %s",
media_path, media_type, media_category)
return media_category, media_type
file = open(media_path, 'rb')
resp = self.upload_media_init(media_type, total_bytes)
if 199 > resp.status_code < 300:
self.log_error_resp(resp)
return None
media_id = resp.json()['media_id']
media_id = self.upload_media_chunked(file, total_bytes, media_id)
resp = self.upload_media_finalize(media_id)
if 199 > resp.status_code < 300:
self.log_error_resp(resp)
return media_id
def upload_media_init(self, media_type, total_bytes):
def upload_media_init(self, media_type, media_category, total_bytes):
"""Upload media, INIT phase."""
resp = self.api.request('media/upload',
return self.api.request('media/upload',
{'command': 'INIT', 'media_type': media_type,
'media_category': media_category,
'total_bytes': total_bytes})
return resp
def upload_media_chunked(self, file, total_bytes, media_id):
"""Upload media, chunked append."""
@@ -128,17 +152,55 @@ class TwitterNotificationService(BaseNotificationService):
return media_id
def upload_media_append(self, chunk, media_id, segment_id):
"""Upload media, append phase."""
"""Upload media, APPEND phase."""
return self.api.request('media/upload',
{'command': 'APPEND', 'media_id': media_id,
'segment_index': segment_id},
{'media': chunk})
def upload_media_finalize(self, media_id):
"""Upload media, finalize phase."""
"""Upload media, FINALIZE phase."""
return self.api.request('media/upload',
{'command': 'FINALIZE', 'media_id': media_id})
def check_status_until_done(self, media_id, callback, *args):
"""Upload media, STATUS phase."""
resp = self.api.request('media/upload',
{'command': 'STATUS', 'media_id': media_id},
method_override='GET')
if resp.status_code != 200:
_LOGGER.error("media processing error: %s", resp.json())
processing_info = resp.json()['processing_info']
_LOGGER.debug("media processing %s status: %s", media_id,
processing_info)
if processing_info['state'] in {u'succeeded', u'failed'}:
return callback(media_id)
check_after_secs = processing_info['check_after_secs']
_LOGGER.debug("media processing waiting %s seconds to check status",
str(check_after_secs))
when = datetime.now() + timedelta(seconds=check_after_secs)
myself = partial(self.check_status_until_done, media_id, callback)
async_track_point_in_time(self.hass, myself, when)
@staticmethod
def media_category_for_type(media_type):
"""Determine Twitter media category by mime type."""
if media_type is None:
return None
if media_type.startswith('image/gif'):
return 'tweet_gif'
elif media_type.startswith('video/'):
return 'tweet_video'
elif media_type.startswith('image/'):
return 'tweet_image'
return None
@staticmethod
def log_bytes_sent(bytes_sent, total_bytes):
"""Log upload progress."""
+21 -6
View File
@@ -15,18 +15,20 @@ from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
REQUIREMENTS = ['sleekxmpp==1.3.2',
'dnspython3==1.15.0',
'pyasn1==0.3.2',
'pyasn1-modules==0.0.11']
'pyasn1==0.3.3',
'pyasn1-modules==0.1.1']
_LOGGER = logging.getLogger(__name__)
CONF_TLS = 'tls'
CONF_VERIFY = 'verify'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENDER): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_RECIPIENT): cv.string,
vol.Optional(CONF_TLS, default=True): cv.boolean,
vol.Optional(CONF_VERIFY, default=True): cv.boolean,
})
@@ -34,18 +36,20 @@ def get_service(hass, config, discovery_info=None):
"""Get the Jabber (XMPP) notification service."""
return XmppNotificationService(
config.get(CONF_SENDER), config.get(CONF_PASSWORD),
config.get(CONF_RECIPIENT), config.get(CONF_TLS))
config.get(CONF_RECIPIENT), config.get(CONF_TLS),
config.get(CONF_VERIFY))
class XmppNotificationService(BaseNotificationService):
"""Implement the notification service for Jabber (XMPP)."""
def __init__(self, sender, password, recipient, tls):
def __init__(self, sender, password, recipient, tls, verify):
"""Initialize the service."""
self._sender = sender
self._password = password
self._recipient = recipient
self._tls = tls
self._verify = verify
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
@@ -53,10 +57,11 @@ class XmppNotificationService(BaseNotificationService):
data = '{}: {}'.format(title, message) if title else message
send_message('{}/home-assistant'.format(self._sender), self._password,
self._recipient, self._tls, data)
self._recipient, self._tls, self._verify, data)
def send_message(sender, password, recipient, use_tls, message):
def send_message(sender, password, recipient, use_tls,
verify_certificate, message):
"""Send a message over XMPP."""
import sleekxmpp
@@ -73,6 +78,10 @@ def send_message(sender, password, recipient, use_tls, message):
self.use_ipv6 = False
self.add_event_handler('failed_auth', self.check_credentials)
self.add_event_handler('session_start', self.start)
if not verify_certificate:
self.add_event_handler('ssl_invalid_cert',
self.discard_ssl_invalid_cert)
self.connect(use_tls=self.use_tls, use_ssl=False)
self.process()
@@ -87,4 +96,10 @@ def send_message(sender, password, recipient, use_tls, message):
"""Disconnect from the server if credentials are invalid."""
self.disconnect()
@staticmethod
def discard_ssl_invalid_cert(event):
"""Do nothing if ssl certificate is invalid."""
_LOGGER.info('Ignoring invalid ssl certificate as requested.')
return
SendNotificationBot()
+29 -25
View File
@@ -4,6 +4,7 @@ Support for RFXtrx components.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/rfxtrx/
"""
import logging
from collections import OrderedDict
import voluptuous as vol
@@ -11,13 +12,14 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
ATTR_ENTITY_ID, TEMP_CELSIUS,
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
)
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pyRFXtrx==0.19.0']
REQUIREMENTS = ['pyRFXtrx==0.20.1']
DOMAIN = 'rfxtrx'
@@ -54,7 +56,7 @@ DATA_TYPES = OrderedDict([
RECEIVED_EVT_SUBSCRIBERS = []
RFX_DEVICES = {}
_LOGGER = logging.getLogger(__name__)
RFXOBJECT = None
RFXOBJECT = 'rfxobject'
def _valid_device(value, device_type):
@@ -77,10 +79,6 @@ def _valid_device(value, device_type):
if not len(key) % 2 == 0:
key = '0' + key
if get_rfx_object(key) is None:
raise vol.Invalid('Rfxtrx device {} is invalid: '
'Invalid device id for {}'.format(key, value))
if device_type == 'sensor':
config[key] = DEVICE_SCHEMA_SENSOR(device)
elif device_type == 'binary_sensor':
@@ -171,24 +169,24 @@ def setup(hass, config):
# Try to load the RFXtrx module.
import RFXtrx as rfxtrxmod
# Init the rfxtrx module.
global RFXOBJECT
device = config[DOMAIN][ATTR_DEVICE]
debug = config[DOMAIN][ATTR_DEBUG]
dummy_connection = config[DOMAIN][ATTR_DUMMY]
if dummy_connection:
RFXOBJECT =\
rfxtrxmod.Connect(device, handle_receive, debug=debug,
hass.data[RFXOBJECT] =\
rfxtrxmod.Connect(device, None, debug=debug,
transport_protocol=rfxtrxmod.DummyTransport2)
else:
RFXOBJECT = rfxtrxmod.Connect(device, handle_receive, debug=debug)
hass.data[RFXOBJECT] = rfxtrxmod.Connect(device, None, debug=debug)
def _start_rfxtrx(event):
hass.data[RFXOBJECT].event_callback = handle_receive
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_rfxtrx)
def _shutdown_rfxtrx(event):
"""Close connection with RFXtrx."""
RFXOBJECT.close_connection()
hass.data[RFXOBJECT].close_connection()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx)
return True
@@ -285,13 +283,16 @@ def find_possible_pt2262_device(device_id):
return None
def get_devices_from_config(config, device, hass):
def get_devices_from_config(config, device):
"""Read rfxtrx configuration."""
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
devices = []
for packet_id, entity_info in config[CONF_DEVICES].items():
event = get_rfx_object(packet_id)
if event is None:
_LOGGER.error("Invalid device: %s", packet_id)
continue
device_id = slugify(event.device.id_string.lower())
if device_id in RFX_DEVICES:
continue
@@ -303,13 +304,12 @@ def get_devices_from_config(config, device, hass):
new_device = device(entity_info[ATTR_NAME], event, datas,
signal_repetitions)
new_device.hass = hass
RFX_DEVICES[device_id] = new_device
devices.append(new_device)
return devices
def get_new_device(event, config, device, hass):
def get_new_device(event, config, device):
"""Add entity if not exist and the automatic_add is True."""
device_id = slugify(event.device.id_string.lower())
if device_id in RFX_DEVICES:
@@ -330,7 +330,6 @@ def get_new_device(event, config, device, hass):
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
new_device = device(pkt_id, event, datas,
signal_repetitions)
new_device.hass = hass
RFX_DEVICES[device_id] = new_device
return new_device
@@ -438,31 +437,36 @@ class RfxtrxDevice(Entity):
if command == "turn_on":
for _ in range(self.signal_repetitions):
self._event.device.send_on(RFXOBJECT.transport)
self._event.device.send_on(self.hass.data[RFXOBJECT]
.transport)
self._state = True
elif command == "dim":
for _ in range(self.signal_repetitions):
self._event.device.send_dim(RFXOBJECT.transport,
brightness)
self._event.device.send_dim(self.hass.data[RFXOBJECT]
.transport, brightness)
self._state = True
elif command == 'turn_off':
for _ in range(self.signal_repetitions):
self._event.device.send_off(RFXOBJECT.transport)
self._event.device.send_off(self.hass.data[RFXOBJECT]
.transport)
self._state = False
self._brightness = 0
elif command == "roll_up":
for _ in range(self.signal_repetitions):
self._event.device.send_open(RFXOBJECT.transport)
self._event.device.send_open(self.hass.data[RFXOBJECT]
.transport)
elif command == "roll_down":
for _ in range(self.signal_repetitions):
self._event.device.send_close(RFXOBJECT.transport)
self._event.device.send_close(self.hass.data[RFXOBJECT]
.transport)
elif command == "stop_roll":
for _ in range(self.signal_repetitions):
self._event.device.send_stop(RFXOBJECT.transport)
self._event.device.send_stop(self.hass.data[RFXOBJECT]
.transport)
self.schedule_update_ha_state()
@@ -0,0 +1,289 @@
"""
Support for AirVisual air quality sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.airvisual/
"""
import asyncio
from logging import getLogger
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (ATTR_ATTRIBUTION, ATTR_STATE, CONF_API_KEY,
CONF_LATITUDE, CONF_LONGITUDE,
CONF_MONITORED_CONDITIONS)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = getLogger(__name__)
REQUIREMENTS = ['pyairvisual==0.1.0']
ATTR_CITY = 'city'
ATTR_COUNTRY = 'country'
ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol'
ATTR_POLLUTANT_UNIT = 'pollutant_unit'
ATTR_TIMESTAMP = 'timestamp'
CONF_RADIUS = 'radius'
MASS_PARTS_PER_MILLION = 'ppm'
MASS_PARTS_PER_BILLION = 'ppb'
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
POLLUTANT_LEVEL_MAPPING = [{
'label': 'Good',
'minimum': 0,
'maximum': 50
}, {
'label': 'Moderate',
'minimum': 51,
'maximum': 100
}, {
'label': 'Unhealthy for Sensitive Groups',
'minimum': 101,
'maximum': 150
}, {
'label': 'Unhealthy',
'minimum': 151,
'maximum': 200
}, {
'label': 'Very Unhealthy',
'minimum': 201,
'maximum': 300
}, {
'label': 'Hazardous',
'minimum': 301,
'maximum': 10000
}]
POLLUTANT_MAPPING = {
'co': {
'label': 'Carbon Monoxide',
'unit': MASS_PARTS_PER_MILLION
},
'n2': {
'label': 'Nitrogen Dioxide',
'unit': MASS_PARTS_PER_BILLION
},
'o3': {
'label': 'Ozone',
'unit': MASS_PARTS_PER_BILLION
},
'p1': {
'label': 'PM10',
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
},
'p2': {
'label': 'PM2.5',
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
},
's2': {
'label': 'Sulfur Dioxide',
'unit': MASS_PARTS_PER_BILLION
}
}
SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'}
SENSOR_TYPES = [
('AirPollutionLevelSensor', 'Air Pollution Level', 'mdi:scale'),
('AirQualityIndexSensor', 'Air Quality Index', 'mdi:format-list-numbers'),
('MainPollutantSensor', 'Main Pollutant', 'mdi:chemical-weapon'),
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY):
cv.string,
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
vol.Optional(CONF_LATITUDE):
cv.latitude,
vol.Optional(CONF_LONGITUDE):
cv.longitude,
vol.Optional(CONF_RADIUS, default=1000):
cv.positive_int,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Configure the platform and add the sensors."""
import pyairvisual as pav
api_key = config.get(CONF_API_KEY)
_LOGGER.debug('AirVisual API Key: %s', api_key)
monitored_locales = config.get(CONF_MONITORED_CONDITIONS)
_LOGGER.debug('Monitored Conditions: %s', monitored_locales)
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
_LOGGER.debug('AirVisual Latitude: %s', latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
_LOGGER.debug('AirVisual Longitude: %s', longitude)
radius = config.get(CONF_RADIUS)
_LOGGER.debug('AirVisual Radius: %s', radius)
data = AirVisualData(pav.Client(api_key), latitude, longitude, radius)
sensors = []
for locale in monitored_locales:
for sensor_class, name, icon in SENSOR_TYPES:
sensors.append(globals()[sensor_class](data, name, icon, locale))
async_add_devices(sensors, True)
def merge_two_dicts(dict1, dict2):
"""Merge two dicts into a new dict as a shallow copy."""
final = dict1.copy()
final.update(dict2)
return final
class AirVisualBaseSensor(Entity):
"""Define a base class for all of our sensors."""
def __init__(self, data, name, icon, locale):
"""Initialize."""
self._data = data
self._icon = icon
self._locale = locale
self._name = name
self._state = None
self._unit = None
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._data:
return {
ATTR_ATTRIBUTION: 'AirVisual©',
ATTR_CITY: self._data.city,
ATTR_COUNTRY: self._data.country,
ATTR_STATE: self._data.state,
ATTR_TIMESTAMP: self._data.pollution_info.get('ts')
}
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def name(self):
"""Return the name."""
return '{0} {1}'.format(SENSOR_LOCALES[self._locale], self._name)
@property
def state(self):
"""Return the state."""
return self._state
@asyncio.coroutine
def async_update(self):
"""Update the status of the sensor."""
_LOGGER.debug('updating sensor: %s', self._name)
self._data.update()
class AirPollutionLevelSensor(AirVisualBaseSensor):
"""Define a sensor to measure air pollution level."""
@asyncio.coroutine
def async_update(self):
"""Update the status of the sensor."""
yield from super().async_update()
aqi = self._data.pollution_info.get('aqi{0}'.format(self._locale))
try:
[level] = [
i for i in POLLUTANT_LEVEL_MAPPING
if i['minimum'] <= aqi <= i['maximum']
]
self._state = level.get('label')
except ValueError:
self._state = None
class AirQualityIndexSensor(AirVisualBaseSensor):
"""Define a sensor to measure AQI."""
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return ''
@asyncio.coroutine
def async_update(self):
"""Update the status of the sensor."""
yield from super().async_update()
self._state = self._data.pollution_info.get(
'aqi{0}'.format(self._locale))
class MainPollutantSensor(AirVisualBaseSensor):
"""Define a sensor to the main pollutant of an area."""
def __init__(self, data, name, icon, locale):
"""Initialize."""
super().__init__(data, name, icon, locale)
self._symbol = None
self._unit = None
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._data:
return merge_two_dicts(super().device_state_attributes, {
ATTR_POLLUTANT_SYMBOL: self._symbol,
ATTR_POLLUTANT_UNIT: self._unit
})
@asyncio.coroutine
def async_update(self):
"""Update the status of the sensor."""
yield from super().async_update()
symbol = self._data.pollution_info.get('main{0}'.format(self._locale))
pollution_info = POLLUTANT_MAPPING.get(symbol, {})
self._state = pollution_info.get('label')
self._unit = pollution_info.get('unit')
self._symbol = symbol
class AirVisualData(object):
"""Define an object to hold sensor data."""
def __init__(self, client, latitude, longitude, radius):
"""Initialize."""
self.city = None
self._client = client
self.country = None
self.latitude = latitude
self.longitude = longitude
self.pollution_info = None
self.radius = radius
self.state = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update with new AirVisual data."""
import pyairvisual.exceptions as exceptions
try:
resp = self._client.nearest_city(self.latitude, self.longitude,
self.radius).get('data')
_LOGGER.debug('New data retrieved: %s', resp)
self.city = resp.get('city')
self.state = resp.get('state')
self.country = resp.get('country')
self.pollution_info = resp.get('current').get('pollution')
except exceptions.HTTPError as exc_info:
_LOGGER.error('Unable to update sensor data')
_LOGGER.debug(exc_info)
+11 -2
View File
@@ -220,7 +220,12 @@ class BrSensor(Entity):
# update all other sensors
if self.type.startswith(SYMBOL) or self.type.startswith(CONDITION):
condition = data.get(FORECAST)[fcday].get(CONDITION)
try:
condition = data.get(FORECAST)[fcday].get(CONDITION)
except IndexError:
_LOGGER.warning("No forecast for fcday=%s...", fcday)
return False
if condition:
new_state = condition.get(CONDITION, None)
if self.type.startswith(SYMBOL):
@@ -240,7 +245,11 @@ class BrSensor(Entity):
return True
return False
else:
new_state = data.get(FORECAST)[fcday].get(self.type[:-3])
try:
new_state = data.get(FORECAST)[fcday].get(self.type[:-3])
except IndexError:
_LOGGER.warning("No forecast for fcday=%s...", fcday)
return False
if new_state != self._state:
self._state = new_state

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