Compare commits

...

139 Commits

Author SHA1 Message Date
Paulus Schoutsen 7461c57542 Merge pull request #8270 from home-assistant/release-0-48
0.48
2017-07-01 16:58:10 -07:00
Paulus Schoutsen 632f9a21b6 Update frontend 2017-07-01 16:53:50 -07:00
Mike Megally da44f80b32 Create an index on the states table to help hass startup time (#8255) 2017-07-01 14:10:39 -07:00
Michael Fester 8fb49e8687 Snips ASR and NLU component (#8156)
* Snips ASR and NLU component

* Fix warning

* Fix warnings

* Fix lint issues

* Add tests

* Fix tabs

* Fix newline

* Fix quotes

* Fix docstrings

* Update tests

* Remove logs

* Fix lint warning

* Update API

* Fix Snips
2017-07-01 13:58:35 -07:00
Will W 5f8dc8af20 components.knx - KNXMultiAddressDevice corrections (#8275)
1. The has_attributes was comparing names to addresses
2. Some errors outside of an except block were using
_LOGGER.except. This will cause an exception itself because there is no
trance context available to the logger
3. Added alias names for the address and state addresses so that they
can be accessed with the same
4. Added return values for the set_int_value and set_percentage methods
to allow error checking similar to the set_value method
5. Added the name of the configured object to the log messages to make
them more meaningful (otherwise multiple similar log messages are
received without any hint as to the target device)
2017-07-01 13:31:01 -07:00
lrmate d267fc608f Update modbus.py (#8256)
* Update modbus.py

Prevents Modbus binary sensors showing up as "unnamed_device".
Originally proposed here https://community.home-assistant.io/t/modbus-sensor/6751/11 by user Pjeter

* Update modbus.py
2017-06-30 23:56:46 -07:00
Paulus Schoutsen d3bc8519c0 Update frontend 2017-06-30 22:34:55 -07:00
viswa-swami d3adc6ddfb Camera services arm disarm including Netgear Arlo (#7961)
* Added camera service calls to arm/disarm the cameras. Entity id is optional so that with a single call we can arm all the cameras or specify a particular entity id to arm if applicable and possible in that camera type.

* Added camera service calls to arm/disarm the cameras. Entity id is optional so that with a single call we can arm all the cameras or specify a particular entity id to arm if applicable and possible in that camera type.

* Added camera service calls to arm/disarm the cameras. Entity id is optional so that with a single call we can arm all the cameras or specify a particular entity id to arm if applicable and possible in that camera type.

* Fixed the spaces and indentation related issues that houndci found

* Fixed the spaces and indentation related issues that houndci found

* Missed the const file which has the macros defined.

* Fixed the CI build error

* Fixed the CI build error because of unused variable in exception case

* Updating the arlo code based on comment from @balloob. Changed the arm and disarm to enable_motion_detection and disable_motion_detection respectively. Similarly fixed the AttributeError handling. Added dummy code to the demo camera also. Moved out the definitions in const.py into the camera __init__ file

* Fixed the comments posted by houndci-bot

* Fixed the comments posted by houndci-bot

* Fixed the comments posted by houndci-bot

* Fixed the comments posted by travis-ci integration bot

* Fixed the comments posted by travis-ci integration bot

* Fixed the comments posted by travis-ci integration bot for demo.py: expected 2 lines, found 1

* Updated code in camera __init__.py to use the get function instead of directly calling the member in the structure.

* Updated code in camera __init__.py

* Posting the updated code for PR based on @balloob's suggestions/recommendations

* Removed the arlo reference from demo code. Copy-paste error

* Removed the unused import found by hound bot

* Expected 2 lines before function, but found only 1.

* Based on @balloob's comments, moved these constants to the camera/arlo.py

* Added test_demo.py to test the motion enabled and motion disabled in camera component

* Fixing issues found by houndci-bot

* Fixing issues found by houndci-bot

* Fixing the code as per @balloob's suggestions

* Fixing the code as per @balloob's suggestions

* Fixing the test_demo failure. Tried to rewrite a base function to enable the motion in __init__.py and missed to add it to as a job.

* Fixing the hound bot comment

* Update arlo.py

* Update arlo.py
2017-06-30 22:24:36 -07:00
Josh a3f586d097 Adding done_message to alert (#8116)
* Adding done_message to alert

Adding an optional entry to the config that will send a notification when an
alarm goes from on to off.

* Update test_alert.py

* Update test_alert.py
2017-06-30 22:24:36 -07:00
Paulus Schoutsen f8c7fd212f Revert "Make Android app shortcut use 'Home Assistant' as name." (#8271)
* Revert "Version bump to 0.49.0.dev0 (#8266)"

This reverts commit 8e4394f173.

* Revert "Adding done_message to alert (#8116)"

This reverts commit 5e56bc7464.

* Revert "Camera services arm disarm including Netgear Arlo (#7961)"

This reverts commit ed20f7e359.

* Revert "Make Android app shortcut use 'Home Assistant' as name instead of just 'Assistant'. (#8261)"

This reverts commit 0bcb7839fb.
2017-06-30 22:24:36 -07:00
Paulus Schoutsen b1f3492fd0 Notify.smtp: default to STARTTLS 2017-06-30 22:15:41 -07:00
Fabian Affolter 74acc5cf41 Merge branch 'master' into dev 2017-06-30 18:56:26 +02:00
Michaël Arnauts 0bcb7839fb Make Android app shortcut use 'Home Assistant' as name instead of just 'Assistant'. (#8261) 2017-06-30 18:51:07 +02:00
PhracturedBlue 17237e9d3f Implement templates for covers (#8100)
* Implement templates for covers

* Fix a few remaining pylint warnings

* Fix hound line-length warnings

* Fix one more hound line-length warning

* Fix quadruple-quotes an line length code-quality issues

* Irrelevant change to retrigger travis due to timeout

* Use volutuous Exclusive to check for mutex condition

* Fix incorrect state check
2017-06-30 08:24:29 -07:00
Michaël Arnauts a663dbada0 Docker cleanup. (#8226) 2017-06-30 08:07:33 -07:00
Fabian Affolter 96e1d5524a Upgrade libnacl to 1.5.1 (#8259) 2017-06-30 11:12:21 +02:00
JudgeDredd 33fd2250fd further document add_node_secure (#8229)
added documentation to *attempt* explanation that add_node_secure will also function for adding unsecure nodes.
2017-06-30 10:00:38 +03:00
Per Sandström 31f17a91e6 verisure component names (#8251) 2017-06-30 08:53:14 +02:00
Andrey d0720ac699 Add PlatformNotReady support for Sensibo (#8252) 2017-06-30 08:50:25 +02:00
Fabian Affolter 05acf1c10a Use constant and update ordering (#8246) 2017-06-30 08:46:22 +02:00
Fabian Affolter 27c92937f2 Use 'hass.data' instead of global (#8245) 2017-06-30 08:46:03 +02:00
Anders Melchiorsen a328df6014 LIFX: Small code cleanups (#8228) 2017-06-30 02:10:28 +02:00
Eugenio Panadero 1fb4eefc2c better logging to debug when a message is not sent (#8248) 2017-06-29 21:13:46 +02:00
Fabian Affolter 0f12b4c955 Do not call update() in constructor (#8247)
Add an optional extended description…
2017-06-29 16:21:29 +02:00
Fabian Affolter a9f14b67a8 Update docstrings (#8244) 2017-06-29 11:44:35 +02:00
Eugenio Panadero 445065700c update i2csense requirement (#8242) 2017-06-29 11:03:52 +02:00
Fabian Affolter 4bd96fd437 Upgrade python-digitalocean to 1.12 (#8241) 2017-06-29 10:52:12 +02:00
Michaël Arnauts 5dde0c2201 Comfoconnect fan component (#8073)
* Comfoconnect fan component.

* Fix linter. Don't store hass object when not needed.

* More code style.

* Rebase to dev and add to coverage ignore list.

* Use published package from pypi.
2017-06-28 18:04:54 +02:00
Open Home Automation 6846a76c46 KNX Cover tilt control (#8159)
* Added invert flag for position for actuators that uses 100% for fully closed position

* Implementation of tilt functionality

* Bugfix check tilt

* Formatting

* Formatting fixes

* Formatting

* Bugfix set_tilt

* Minor modifications in configuration section

* Formatting

* Update knx.py
2017-06-28 14:08:07 +02:00
Fabian Affolter fa6e93f0c7 Do not call update() in constructor (#8148)
* Do not call update() in constructor

* Move handling to update and re-add throttle

* Fix indent

* Fix interval
2017-06-27 10:56:25 +02:00
Paulus Schoutsen 5ef274adce Cleanup automations yaml (#8223) 2017-06-27 10:36:26 +02:00
Eugenio Panadero e39f7d3ef5 Fix homeassistant.start trigger (#8220)
* Fix homeassistant.start trigger

* ooops

* set sleep(0) just before changing to running state, revert async_block_till_done changes
2017-06-27 10:36:00 +02:00
Will W 88b9503962 add percentage (DPT_Scaling) KNX sensors (#8168)
* add percentage (DPT_Scaling) KNX sensors

1. moved basic functionality to KNXSensorBaseClass instead of
KNXSensorFloatClass
2. added "if" clause in setup for a "percentage" sensor type and added KNXSensorDPTScalingClass

* support-knx-percentage-sensor: lint correction

Updated convert method base sensor class to avoid lint warning
(R201 - Method could be a function)

* added PLATFORM_SCHEMA for configuration

1. added SCHEMA extension for defined keywords
2. moved fixed data for internal settings out of sensor logic
3. moved everything into standard KNXSensor object
4. added parsing of extra config parameters in __init__

* correct lint errors on support-knx-percentage-sensor
2017-06-26 22:25:54 -07:00
Eugenio Panadero 596093d564 telegram_bot platform to only send messages (#8186)
* add new telegram_bot platform to only send messages

* Fix async
2017-06-26 22:22:33 -07:00
natemason 23400c4b0a Fixed mqtt subscription filter on sys $ topics (#8166)
* Fixed mqtt subscription filter on sys $ topics

* fixed linting issue

* added unit tests for $ topics and changed fix to use re.escape

* merge upstream/dev mqtt unit tests

* Update test_init.py
2017-06-26 22:17:55 -07:00
Anders Melchiorsen af54311718 LIFX: Move light effects to external library (#8222)
* LIFX: Move light effects to external library

This moves the LIFX light effects to the external library aiolifx_effects.

To get the light state synchronized between that library and HA, the LIFX
platform no longer maintains the light state itself. Instead, it uses the
cached state that aiolifx maintains.

The reorganization also includes the addition of a cleanup handler.

* Fix style
2017-06-26 22:05:32 -07:00
Pascal Vizeli 442dcd584b Improve executor pool size / speedup python 3.5 (#8215)
* Improve executor pool size / speedup python36

* fix style

* Add comment
2017-06-26 18:18:42 -07:00
Eugenio Panadero 1e4aec63ed guess the content_type in local_file cameras (#8217)
* guess the content_type in local_file cameras

* add unittest to check content_type of local_file cameras
2017-06-26 22:36:35 +02:00
Per Sandström 80c187f8ea WIP: Verisure app api (#7394)
update to verisure app api
2017-06-26 22:30:25 +02:00
Paulus Schoutsen d73b695e73 EntityComponent to retry platforms that are not ready yet (#8209)
* Add PlatformNotReady Exception

* lint

* Remove cap, adjust algorithm
2017-06-26 09:41:48 -07:00
Adam Baxter f02d169864 Fix Plex component to use port number in discovery. (#8197)
* Fix Plex component to use port number in discovery.

* Break line

* Correctly save port to config

* Handle port with fewer code changes

* This is stuck configuring and I'm not sure why

* Changes suggested by @dale3h
2017-06-25 18:06:15 -05:00
Pascal Vizeli 2dd7f0616e Add security layer to send file output things (#8189)
* Add security layer to send file output things

* Make telegram secure

* fix lint

* fix handling

* invert check

* resolve relative paths

* add test for relative paths

* fix lint

* fix tests

* Address paulus comments

* fix style

* fix tests

* Add more tests

* fix tests

* fix tests

* fix test p2

* fix lint

* fix tests

* Make it available for windows

* Change name / address comments

* fix set

* fix test

* fix tests

* fix test

* fix lint
2017-06-26 00:10:30 +02:00
Wim Haanstra 2f2952e0ec Openhardwaremonitor (#8056)
* Open Hardware Monitor sensor

Platform which is able to connect to the JSON API of Open Hardware Monitor and adds sensors for the devices.

* Remove copyright in header, not needed.

* - Removed old code
- Fixed typo’s in comments
- Removed log spamming
- Removed code that was unnecessary
- Use requests instead of urllib
- Moved sensor update functionality to data handler, to remove unwanted constructor parameters

* Fixed typo in comment
Added tests

* Added default fixture, to stabilize tests

* - Fix for values deeper than 4 levels, no longer relies on fixed level
- Fixed tests

* Removed timer in preference of helper methods

* Moved update functionality back to Entity….
Updated SCAN INTERVAL

* Added timeout to request
Removed retry when Open Hardware Monitor API is not reachable
Fixed naming of sensors
Flow optimalisations
Fixed tests to use states

* Remove unused import
2017-06-25 13:48:05 -07:00
Adam Mills 8358542ce0 Remove unnecessary thread_ident assignment (#8194)
* Remove mocking of _thread_ident

* Re-add run_loop thread_ident assignment
2017-06-25 16:39:05 -04:00
Eugenio Panadero 4ca5ed25bc add option to set content_type in camera.generic to support 'svg cameras' (#8188)
* add custom content_type to support 'generic svg cameras'

* add unittest to check content_type for svg generic camera

* Tweak tests
2017-06-25 12:25:14 -07:00
Paulus Schoutsen 7bf6ceafec Split mock_service (#8198) 2017-06-25 10:53:15 -07:00
Paulus Schoutsen 1cfed4f015 Fix plants calling async methods from sync context (#8200) 2017-06-25 10:07:28 -07:00
Paulus Schoutsen a082ffca1d Fix MySensors climate (#8193) 2017-06-24 18:11:34 -07:00
Oliver 1b563b0640 Pushed to version 0.5.1 of the library (#8190) 2017-06-24 12:14:57 -07:00
Adam Mills 1fe189e9cb Switch to new zwave entity ids by default (#8192) 2017-06-24 15:01:57 -04:00
Marc Plano-Lesay edeb92ea42 Add offset option to sensor.gtfs (#7980)
* Add offset option to sensor.gtfs

* Fix long lines in sensor.gtfs

* Expose GTFS offset as an attribute
2017-06-24 17:45:14 +02:00
Alex Mekkering c1095665e9 added optional node_id to MQTT discovery (#8096) 2017-06-24 00:46:41 -07:00
Kane610 2a1f8af10a Axis service vapix call (#7794)
* Initial commit for an Axis service to do Vapix calls to device

* Added check to see if metadatastream initiated properly

* Make sure to configure the correct IP address when setting up registered devices on system start

* Manage reconnection when device is discovered with a different IP

* Cleaned up setting new IP

* Better naming of event for new IP

* New version of dependency axis

* Fix flake8 failing

* Break out service default strings to constants

* Use the dispatcher and not the core event bus for internal communication
2017-06-24 00:14:57 -07:00
Bas Schipper 6234f2d73f Added buienradar precipitation forecast average & total sensors (#8171)
* Added precipitation forecast average & total sensors

* Fixed some code style issues

* Fixed some code style issues

* Minor fix default timeframe

* Update buienradar.py

* Update buienradar.py
2017-06-24 00:12:52 -07:00
Paulus Schoutsen b488663f2c Update Dockerfile 2017-06-23 23:13:38 -07:00
Sean Dague a55d8776ff Throw exception if _convert_for_display called on non Number (#8178)
In trying to come up for some reason behind issue #6365 (which only
happens on some platforms) the best guess is that some components are
managing to get a string value all the way up to the Polymer UI for
temperature, which then an increment of +0.5 is treating as a string
concat operation instead of addition. So 20 + 0.5 becomes 200.5 hits
the max thermostat value.

This will throw an exception if the climate temp value isn't a
number. That's going to turn a soft fail into a hard fail on
potentially a number of platforms. Mysensors is one of the platforms
that was reported as having the issue. So put some explicit float
casts where that might be coming from as well.
2017-06-23 23:03:37 -07:00
Ryan Nowakowski 5ceb4c404d Fix radiotherm model CT50 (#8181)
Model CT50 has an "Auto" mode.  When mode is set to auto we need to ask
what the current state is: cool or heat.  Then we can query the
appropriate target temperature.

Without this fix, the target temperature shows up blank in the UI and
setting the mode fails.
2017-06-23 22:53:10 -07:00
lrmate 0061cece0c Update buienradar.py (#8173)
Swapped unit of measurement 'winddirection' vs 'windazimuth'
2017-06-23 22:51:45 -07:00
Morten Lied Johansen 0099168ff8 Add device tracker for Linksys Smart Wifi devices (#8144)
* Add device tracker for Linksys Smart Wifi devices

* Fixing code style
2017-06-23 22:36:04 -07:00
Paulus Schoutsen 87c89752ab Revert "Add libboost-python1.62-dev (fixes #7851)" (#8182)
* Revert "Uninstall enum34 in python3.6 docker image (#8103)"

This reverts commit 45f6f4443a.

* Revert "Add libboost-python1.62-dev (fixes #7851) (#7868)"

This reverts commit f1290d3135.
2017-06-23 22:33:33 -07:00
Jean Regisser 45f6f4443a Uninstall enum34 in python3.6 docker image (#8103)
* Uninstall enum34 in python3.6 docker image

This is a short term fix for #7733

What's happening is the following dependencies are pulling enum34:
- pygatt
- libsoundtouch
- yeelight
However, enum34 is not meant to be installed in Python versions 3.4+
and causing the `AttributeError: module 'enum' has no attribute 'IntFlag'``

I've submitted patches to these projects so we don't have to do this
manual uninstall in the future.

* Update Dockerfile
2017-06-23 22:29:39 -07:00
Fabian Affolter f1290d3135 Add libboost-python1.62-dev (fixes #7851) (#7868) 2017-06-23 22:16:19 -07:00
Omar Usman 746aae51ec Add ClickSend notify service. (#8135)
* Add ClickSend notify service.

* PR #8135 changes.

- Some code spacing fixes.
- Add timeout to requests.
- Change doc url.
- Use const.py as much as possible.
- Check credentials to determine if setup fails or not.
- Add docstrings.
- Use string formatting.

* PR #8135 changes.

- Remove unused variables.
- Continuation line under-indented for visual indent.

* PR #8135 changes.

- Format code based on PEP8.

* PR #8135 changes.

- Remove unused base64 dependency.

* PR #8135 changes.

- Fix: D205: 1 blank line required between summary line and description (found 0)
- Fix: standard import "import json" comes before "import requests"

* PR #8135 changes.

- Add files to .coveragerc

* Remove obvious comments and set constant
2017-06-23 22:51:41 +02:00
Wolfgang Malgadey da9430ed12 Tado climate device (#8041)
* added default parameter

* zone overlay can be set with or without a temperature and with or without a duration. Duration is not supported by hass

* Fixes issue #7059 with missing sensorDataPoints

* Fixes issue #6943 added ac_mode

* ac_mode cases
* added fan modes
* changed handling of device state OFF

* fixed an error initializing a dictionary (#6943)

* changed pytado version

* activated pytado debugging

* Changed pyTado version

* mytado.com changed authentication challenge

* Fixed linelength and whitespace issues

* requirements to pytado changed
2017-06-23 18:45:44 +02:00
Tim Wilde bef22076ea Use version 1.3 of radiotherm (#8164)
Add an optional extended description…
2017-06-23 11:08:23 +02:00
Bas Schipper fe93b51017 Fixed rfxtrx binary_sensor off command (#8160)
* Fixed applying rfxtrx binary off command

* Fixed some deprecation issues
2017-06-22 23:00:44 +02:00
Eugenio Panadero 07293e8d1e add telegram_bot service: delete_message (#8153)
* add telegram_bot service: delete_message

* better validating for `last` message_id option
2017-06-22 15:03:11 +02:00
Open Home Automation ca71d34076 Added invert flag for position for actuators that uses 100% for fully closed position (#8147) 2017-06-22 13:42:13 +02:00
Anton Lundin 548417761e ubus: Refresh session on Access denied (#8111)
When a openwrt router reboots, all the session ids gets invalidated.
In that case we need to log in again and get a new session id.

Signed-off-by: Anton Lundin <glance@acc.umu.se>
2017-06-22 13:34:57 +02:00
Andrey 7b8ad1d365 Switch rachiopy to pypi (#8040)
* Switch rachiopy t pypi

* Update rachio.py

* Update requirements_all.txt
2017-06-22 13:34:00 +02:00
Fabian Affolter 61cb6ec3dc Upgrade libsoundtouch to 0.6.2 (#8149) 2017-06-22 13:27:42 +02:00
Fabian Affolter 349746f5f2 Upgrade python-telegram-bot to 6.1.0 (#8151) 2017-06-22 13:27:02 +02:00
Oliver 2e3b279873 Add support of Zone2 and Zone3 (#8025)
* Add support of Zone2 and Zone3

* Changes from balloobs feedback
2017-06-21 22:54:10 -07:00
Yannick POLLART f26861976d Rfxtrx binary sensor (#6794)
* Added rfxtrx binary sensors to a new branch

* binary_sensor/rfxtrx: added support for automatic_add

* Fixed pylint warnings

* off_delay is set wit clearer time specifiers (cv.time_period)

* fire_event config attribute is now supported by rfxtrx binary sensors.

* Cosmetic ordering

* Fix lint errors for PR requirements.

* Fixed indents, line length and comment problems.

* Yet another line too long fix...

* Using existing attributes and config constants.

* Cosmetic fix (ATTR_DATABITS -> ATTR_DATA_BITS)

* Removed unused attribute

* FIX masked device id logging message

* FIX line too long

* FIX trailing white space

* FIX: rfxtrx binary_sensor manages its own devices only.

* Added a basic config helper for pt2262 devices

* Make pylint happy

* Fixed most houndci-bot-detected issues

* Fix TOX complaint about blank line after function docstring

* Fix data bit value calculation

* Fixed line too long

* Removed unnecessary code.

* remove trailing whitespace

* Added hass property to device object.
2017-06-21 22:48:45 -07:00
Anthony Hughes 6bfeac7f80 Harmony auto discovery via netdisco (#7741)
* Use netdisco to automatically discover harmony hubs.

* Allow some settings in configuration.yml to override even on discovered hubs.

* Global is not required as no assignment on variable

* Use `set` instead of list for perf

* Store cache of discovered devices against `hass.data` rather than in global

* Handle case if the device cache is empty

* Fix indentation issue
2017-06-21 22:43:35 -07:00
Miha Lunar a95fe588ca LimitlessLED: Configurable fade-out behavior (#7369)
* Configurable fade-out behavior

Adds a per-group "fade" option with values of "out" (default) or "none".
By default, the lights are faded out when turned off, but this can cause usability issues when manually switching wall switches, since the bulbs turn back on at minimum brightness.

* Changed fade value from enum to boolean

* No need to fall back to default since voluptuous takes care of that.
2017-06-21 22:22:24 -07:00
Eugenio Panadero e5d11dd1a5 Add new BH1750 light level sensor (#8050)
* new sensor platform
* requirements_all and .coveragerc update
2017-06-22 07:09:08 +02:00
Eugenio Panadero 435e5c8a91 Add I2c HTU21D temperature and humidity sensor for Raspberry Pi (#8049)
* Add new HTU21D temperature and humidity sensor

* new sensor platform
* requirements_all and .coveragerc update

* fix lint

* review changes: move sensor code to external module

* remove debug log msg

* add i2csense to COMMENT_REQUIREMENTS, require i2csense 0.0.3

* Add new HTU21D temperature and humidity sensor

* new sensor platform
* requirements_all and .coveragerc update

* fix lint

* review changes: move sensor code to external module

* remove debug log msg

* add i2csense to COMMENT_REQUIREMENTS, require i2csense 0.0.3

* change style for hass

* fix requirements
2017-06-22 07:05:58 +02:00
Jose Juan Montes 8d0553d9e6 Adds CPU temp monitoring, and allow startup when endpoint is not yet available. (#8093)
* Adds CPU temp monitoring, and allow startup when endpoint is not yet available.

* Added support for available() to glances sensor.
2017-06-21 22:45:15 +02:00
Charles Blonde 9a239d1afb Upgrade libsoundtouch to prevent Python3.6 errors with enum. #7733 #8103 (#8143) 2017-06-21 22:20:30 +02:00
Paulus Schoutsen 9252854f99 Merge pull request #8141 from home-assistant/release-0-47-1
0.47.1
2017-06-21 09:24:12 -07:00
Paulus Schoutsen 8d76e2679d Allow iteration in python_script (#8134)
* Allow iteration in python_script

* Add tests
2017-06-21 09:09:08 -07:00
Alan Fischer 4b1dcad7ae Fixed iTach command parsing with empty data (#8104)
* Fixed iTach command parsing with empty data

* Switched to using format
2017-06-21 09:09:08 -07:00
Phil Hawthorne b45c386fd6 Update InfluxDB to handle datetime objects and multiple decimal points (#8080)
* Update InfluxDB to handle datetime objects

Updates the InfluxDB regex to ignore datetime objects being coverted
into float values.

Adds tests to the component to ensure datetime objects are corectly
handled.

* Fix Hound errors

Fixes errors from Hound bot

* Update InfluxDB to handle multiple decimal points

Changes the way InfluxDB handles values such as 1.2.3.4 to be 1.234 so
it stores in InfluxDB as a valid float value

* Fix lint issues

Reduce the size of a line for the linter

* Update InfluxDB to pass on unknown variable

If we get an error trying to convert a variable to a float, let's ignore
it completely

* Make InfluxDB Regex constants

Makes the Regex's used by InfluxDB constants so they don't need to be
compiled each time

* cleanup

* fix lint

* Update regex

* fix tests

* Fix JSON body missing new line character

* fix exceptions
2017-06-21 09:09:08 -07:00
Charles Blonde cb5fa79835 Fix Dyson async_add_job (#8113) 2017-06-21 09:09:08 -07:00
Tsvi Mostovicz b74217bec2 Fix lights issue #8098 (#8101)
* Fix lights issue #8098

* Don't check self._color to decide whether to calll get_color()

self._color is None on init, so get_color() will never be called.
2017-06-21 09:09:08 -07:00
Paulus Schoutsen fcf60e740d Version bump to 0.47.1 2017-06-21 09:08:20 -07:00
Eugenio Panadero bb05600010 Add I2c BME280 temperature, humidity and pressure sensor for Raspberry Pi (#7989)
* Add new BME280 temperature, humidity and pressure sensor

* Add BME280 sensor to optional requirements and .coveragerc

* move validation to sensor handler, async fix in setup

* fix Invalid attribute name

* review changes: move sensor code to external module

* async fix

* add i2csense to COMMENT_REQUIREMENTS, require i2csense 0.0.3, round prec to 1 dec

* change style for hass

* fix lint

* fix lint part 2
2017-06-21 17:24:39 +02:00
Thibault Cohen d3bb6d3988 Decora light: Fix brightness level in UI (#8139) 2017-06-21 16:37:27 +02:00
Thibault Cohen f3945147a4 Add current balance to hydroquebec sensor (#8138) 2017-06-21 16:36:20 +02:00
Paulus Schoutsen 6398e92836 Allow iteration in python_script (#8134)
* Allow iteration in python_script

* Add tests
2017-06-21 13:32:50 +02:00
Steven Conaway 4d2b79156d Change Error Message when Turning off ISY994 Light (#8131) 2017-06-21 09:38:12 +02:00
Fabian Affolter b6d335f993 Do not call update() in constructor (#8120) 2017-06-21 09:35:44 +02:00
lunar-consultancy 4b82c34b8f Added RFXTRX UV badge (#8129) 2017-06-21 08:55:51 +02:00
Paulus Schoutsen 66fc852363 Update frontend 2017-06-20 21:10:53 -07:00
Paulus Schoutsen 87274879a8 Upgrade RestrictedPython dependency (#8132) 2017-06-20 19:30:01 -07:00
Fabian Affolter e4dbf8033c Upgrade aiohttp to 2.2.0 (#8121) 2017-06-21 00:35:49 +02:00
Fabian Affolter 43db94d62d Upgrade sqlalchemy to 1.1.11 (#8124) 2017-06-21 00:32:49 +02:00
Fabian Affolter 6d5fca2db1 Upgrade paho-mqtt to 1.3.0 (#8125) 2017-06-21 00:32:04 +02:00
Fabian Affolter d5e55448ef Upgrade mutagen to 1.38 (#8126) 2017-06-21 00:29:35 +02:00
Alan Fischer 4ad998378f Fixed iTach command parsing with empty data (#8104)
* Fixed iTach command parsing with empty data

* Switched to using format
2017-06-20 15:26:18 +02:00
Fabian Affolter d46607c0d0 Add option to specify the location of the API (fixes #8115) (#8118) 2017-06-20 14:09:54 +02:00
Luar Roji 04920fa0bf Only mark active DHCP clients as present (#8110)
We only want to know which of the DHCP clients are indeed active.

For example: I've a table of static DHCP leases with most of the IPs of my network, so this module is always detecting them as present. With my patch only the active ones will be detected as present.

I already mentioned here: https://github.com/home-assistant/home-assistant/pull/7366#issuecomment-302950139
2017-06-20 12:16:56 +02:00
Fabian Affolter 1928da1fae Remove config details (see docs) (#8119) 2017-06-20 12:09:42 +02:00
Phil Hawthorne 06b051c53d Update InfluxDB to handle datetime objects and multiple decimal points (#8080)
* Update InfluxDB to handle datetime objects

Updates the InfluxDB regex to ignore datetime objects being coverted
into float values.

Adds tests to the component to ensure datetime objects are corectly
handled.

* Fix Hound errors

Fixes errors from Hound bot

* Update InfluxDB to handle multiple decimal points

Changes the way InfluxDB handles values such as 1.2.3.4 to be 1.234 so
it stores in InfluxDB as a valid float value

* Fix lint issues

Reduce the size of a line for the linter

* Update InfluxDB to pass on unknown variable

If we get an error trying to convert a variable to a float, let's ignore
it completely

* Make InfluxDB Regex constants

Makes the Regex's used by InfluxDB constants so they don't need to be
compiled each time

* cleanup

* fix lint

* Update regex

* fix tests

* Fix JSON body missing new line character

* fix exceptions
2017-06-20 07:53:13 +02:00
Charles Blonde 473d765bb9 Fix Dyson async_add_job (#8113) 2017-06-19 23:50:27 +02:00
sn0oz 8e34c27b63 Added SMTP SSL/TLS support (#7960)
* Added SMTP SSL/TLS support

* added new encryption option

* validation of encryption option

* Fix lint issues

* Rename var
2017-06-19 14:19:31 +02:00
Eugenio Panadero 77aa2e940d increase timeout for setWebhook to 10s (#8102)
Add an optional extended description…
2017-06-19 12:03:58 +02:00
Tsvi Mostovicz 3bbaf37193 Fix lights issue #8098 (#8101)
* Fix lights issue #8098

* Don't check self._color to decide whether to calll get_color()

self._color is None on init, so get_color() will never be called.
2017-06-19 09:54:13 +02:00
Andrey b2d6ff9783 More updates to zwave services.yaml file (#8083) 2017-06-18 22:38:14 -07:00
Will W 4fdde4f0e2 add knx cover support (#7997)
* add knx cover

also corrected bugs in device config

1. overwriting of addresses in KNXMultiAddressDevice
2. setting and getting int values
3. added percentage scaling

* Update __init__.py
2017-06-18 22:30:39 -07:00
Jeff Wilson 756768e745 Add support for Insteon FanLinc fan (#6959)
* Add support for Insteon FanLinc fan

* Upgrade insteonlocal dependency to 0.49

* Lint/flake fixes

* Remove configurator

* Make Hound fixes

* Revert "Make Hound fixes" and "Remove configurator"

This reverts commit 04d1f7fdb1.
This reverts commit 7b8278d7cf.
2017-06-18 21:43:10 -07:00
Diogo Gomes 83b791489b Upnp properties (#8067)
* make port mapping optional

* dependencies + improvements

* Added bytes and packets sensors from IGD

* flake8 check

* new sensor with upnp counters

* checks

* whitespaces in blank line

* requirements update

* added sensor.upnp to .coveragerc

* downgrade miniupnpc

Latest version of miniupnpc is 2.0, but pypi only has 1.9

Fortunately it is enough

* revert to non async

miniupnpc will do network calls, so this component can’t be moved to
coroutine

* hof hof

forgot to remove import ot asyncio
2017-06-18 21:32:39 -07:00
Eugenio Panadero 35132f9836 media player Kodi: handle TransportError exceptions when calling JSONRPC API methods (#8047)
* handle TransportError exceptions when calling JSONRPC API

* use double quotes for log messages; show TransportErrors as in async_ws_connect

* fix spaces around keyword / parameter

* fix logging message

* review changes
2017-06-18 23:00:02 +02:00
Paulus Schoutsen 0e08785373 Update frontend 2017-06-18 11:37:15 -07:00
Per Osbäck bf0dbdfd6a update pywebpush to 1.0.5 (#8084) 2017-06-18 10:51:37 -07:00
Michaël Arnauts 04407b8623 Cleanup .coveragerc (#8088) 2017-06-18 10:50:35 -07:00
Michael Auchter fb0ee34f10 mpd: implement support for seek, shuffle, and clear playlist (#8090)
* mpd: add shuffle and clear_playlist support

* mpd: implement seek
2017-06-18 18:31:45 +02:00
Myles Eftos ef63cfe8e4 Stopping the logfile spam by piping STDERR to /dev/null (#8081) 2017-06-18 11:44:41 +02:00
Paulus Schoutsen e40f72e773 Merge branch 'master' into dev 2017-06-17 12:13:59 -07:00
Paulus Schoutsen cec8ccb1a4 Version bump to 0.48.0.dev0 2017-06-17 12:13:46 -07:00
Caleb 6a017efc0e Update pyunifi component to use APIError passed from pyunifi 2.13. Better accommodate login failures with wrapper in pyunifi 2.13. (#7899)
* Pyunifi update

* Update pyunifi_test

* Import API Error

* Adjust test_unifi.py to import APIError

* Remove urllib import

* Remove urllib import from test

* Try fix mock

* Remove automations.yaml

* Lint
2017-06-17 11:09:27 -07:00
Paulus Schoutsen 18935440ed Fix EntityComponent handle entities without a name (#8065)
* Fix EntityComponent handle entities without a name

* Implement solution by Anders
2017-06-17 10:50:59 -07:00
Lev Aronsky 2ba6b3a2ab Added 'all_plants' group and support for plant groups state. (#8063)
* Added 'all_plants' group and support for plant groups state.

* Reversed the group states.
2017-06-17 10:22:23 -07:00
Pascal Vizeli 2438c6b7c2 Fix attribute entity (#8066)
* Bugfix entity attribute setter

* Fix tests

* Fix tests part 2

* Change filter only None

* Fix tests part 3

* Update entity.py

* Fix tests
2017-06-17 10:03:49 -07:00
Andrey 32a84f1466 Add to zwave services descriptions (#8072) 2017-06-17 10:02:37 -07:00
happyleavesaoc 0002a895ca bump usps version (#8074) 2017-06-17 18:42:56 +02:00
happyleavesaoc d0b43b187a bump ups (#8075) 2017-06-17 18:42:12 +02:00
John Mihalic 33d381731f Bump pyEmby version to account for API changes (#8070) 2017-06-17 18:41:35 +02:00
Eugenio Panadero 18f81d7824 Add option to set language of openweathermap sensor, and handle updating errors (#8046)
* Add option to set language of openweathermap sensor messages

* handle error updating openweathermap sensor
2017-06-17 12:37:34 +02:00
Fabian Affolter 844c8149d7 Add initial support for Shiftr.io (#7974)
* Add initial support for Shiftr.io

* Fix lint issue

* Use paho-mqtt instead of internal MQTT object

* remove async flavor while paho is not async
2017-06-17 12:34:12 +02:00
pezinek 7617864ba5 Failed to parse response from WU API: 'record' (and 'recordyear') #7747 (#8058) 2017-06-17 12:32:22 +02:00
jshore1296 58c234466c Allow config of latitude and longitude (#8068)
This will allow for dynamically updating weather states, for instance if
you wanted to use the latitude and longitude of a phone or other device
to get the weather for your current location.
2017-06-17 10:41:11 +02:00
Phil Hawthorne 9071946e87 Remove % sign from Vera Battery Levels (#8069)
Vera devices are reporting battery levels as a sting by appending a
percentage sign (%) on the end.

To make the Vera component act like other Home Assistant components,
let's remove the percentage sign from the battery report levels so that
we only display the battery level.

This may be a "breaking change" if people are relying on the Vera
battery levels to be a string instead of an int. However, this will make
the battery level reports compatible with everything else.
2017-06-17 10:38:15 +02:00
Paulus Schoutsen b24aa24f6a Always enable monkey patch (#8054) 2017-06-16 17:17:18 -07:00
Andrey 1fde234c78 Fix some warnings found by quantifiedcode (#8027)
* Cleanup of warnings by quantifiedcode

* Fix lint

* Fix test

* Delete insteon_hub component

* Also update .coveragerc
2017-06-16 22:44:14 +03:00
Adam Mills afb9cba806 Use standard entity_ids for zwave entities (#7786)
* Use standard entity_ids for zwave entities

* Include temporary opt-in for new entity ids

* Update link to blog post

* Update tests

* Add old entity_id as state attribute

* Expose ZWave value details

* Update tests

* Also show new_entity_id

* Just can't win with this one
2017-06-16 13:07:17 -04:00
pezinek 1c2f4866e2 No update in MQTT Binary Sensor #7478 (#8057) 2017-06-16 14:55:59 +02:00
Pascal Vizeli e90ae2fb75 Update numpy 1.13.0 (#8059) 2017-06-16 11:47:48 +02:00
180 changed files with 6512 additions and 3479 deletions
+49 -41
View File
@@ -35,23 +35,35 @@ omit =
homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py
homeassistant/components/dweet.py
homeassistant/components/*/dweet.py
homeassistant/components/eight_sleep.py
homeassistant/components/*/eight_sleep.py
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py
homeassistant/components/google.py
homeassistant/components/*/google.py
homeassistant/components/insteon_hub.py
homeassistant/components/*/insteon_hub.py
homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py
@@ -65,12 +77,18 @@ omit =
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/joaoapps_join.py
homeassistant/components/*/joaoapps_join.py
homeassistant/components/juicenet.py
homeassistant/components/*/juicenet.py
homeassistant/components/kira.py
homeassistant/components/*/kira.py
homeassistant/components/knx.py
homeassistant/components/*/knx.py
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
@@ -80,15 +98,27 @@ omit =
homeassistant/components/mailgun.py
homeassistant/components/*/mailgun.py
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
homeassistant/components/modbus.py
homeassistant/components/*/modbus.py
homeassistant/components/mysensors.py
homeassistant/components/*/mysensors.py
homeassistant/components/neato.py
homeassistant/components/*/neato.py
homeassistant/components/nest.py
homeassistant/components/*/nest.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py
@@ -116,6 +146,9 @@ omit =
homeassistant/components/scsgate.py
homeassistant/components/*/scsgate.py
homeassistant/components/tado.py
homeassistant/components/*/tado.py
homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py
@@ -148,45 +181,18 @@ omit =
homeassistant/components/wink.py
homeassistant/components/*/wink.py
homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/neato.py
homeassistant/components/*/neato.py
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/knx.py
homeassistant/components/*/knx.py
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/tado.py
homeassistant/components/*/tado.py
homeassistant/components/zha/__init__.py
homeassistant/components/zha/const.py
homeassistant/components/*/zha.py
homeassistant/components/eight_sleep.py
homeassistant/components/*/eight_sleep.py
homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py
@@ -224,11 +230,11 @@ omit =
homeassistant/components/climate/sensibo.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py
homeassistant/components/cover/myq.py
homeassistant/components/cover/opengarage.py
homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py
homeassistant/components/cover/wink.py
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
@@ -242,6 +248,7 @@ omit =
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/linksys_ap.py
homeassistant/components/device_tracker/linksys_smart.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/mikrotik.py
homeassistant/components/device_tracker/netgear.py
@@ -263,12 +270,10 @@ omit =
homeassistant/components/fan/mqtt.py
homeassistant/components/feedreader.py
homeassistant/components/foursquare.py
homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py
homeassistant/components/light/avion.py
@@ -278,7 +283,7 @@ omit =
homeassistant/components/light/flux_led.py
homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx/*.py
homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py
@@ -312,7 +317,6 @@ omit =
homeassistant/components/media_player/frontier_silicon.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py
homeassistant/components/media_player/hdmi_cec.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
@@ -342,13 +346,13 @@ omit =
homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/ciscospark.py
homeassistant/components/notify/clicksend.py
homeassistant/components/notify/discord.py
homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py
@@ -379,8 +383,10 @@ omit =
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py
homeassistant/components/sensor/bh1750.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/blockchain.py
homeassistant/components/sensor/bme280.py
homeassistant/components/sensor/bom.py
homeassistant/components/sensor/broadlink.py
homeassistant/components/sensor/buienradar.py
@@ -419,6 +425,7 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hddtemp.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py
@@ -473,6 +480,7 @@ omit =
homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py
homeassistant/components/sensor/usps.py
homeassistant/components/sensor/vasttrafik.py
@@ -480,6 +488,7 @@ omit =
homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/zamg.py
homeassistant/components/shiftr.py
homeassistant/components/spc.py
homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.py
@@ -489,7 +498,6 @@ omit =
homeassistant/components/switch/dlink.py
homeassistant/components/switch/edimax.py
homeassistant/components/switch/fritzdect.py
homeassistant/components/switch/hdmi_cec.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/hook.py
homeassistant/components/switch/kankun.py
+13 -1
View File
@@ -1,2 +1,14 @@
.tox
# General files
.git
.github
config
# Test related files
.tox
# Other virtualization methods
venv
.vagrant
# Temporary files
**/__pycache__
+9 -1
View File
@@ -1,3 +1,7 @@
# Notice:
# When updating this file, please also update virtualization/Docker/Dockerfile.dev
# This way, the development image and the production image are kept in sync.
FROM python:3.6
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
@@ -21,8 +25,12 @@ RUN virtualization/Docker/setup_docker_prereqs
# Install hass component dependencies
COPY requirements_all.txt requirements_all.txt
# Uninstall enum34 because some depenndecies install it but breaks Python 3.4+.
# See PR #8103 for more info.
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet && \
pip3 uninstall -y enum34
# Copy source
COPY . .
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.verisure/
"""
import logging
from time import sleep
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.verisure import HUB as hub
@@ -20,20 +21,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Verisure platform."""
alarms = []
if int(hub.config.get(CONF_ALARM, 1)):
hub.update_alarms()
alarms.extend([
VerisureAlarm(value.id)
for value in hub.alarm_status.values()
])
hub.update_overview()
alarms.append(VerisureAlarm())
add_devices(alarms)
def set_arm_state(state, code=None):
"""Send set arm state command."""
transaction_id = hub.session.set_arm_state(code, state)[
'armStateChangeTransactionId']
_LOGGER.info('verisure set arm state %s', state)
transaction = {}
while 'result' not in transaction:
sleep(0.5)
transaction = hub.session.get_arm_state_transaction(transaction_id)
# pylint: disable=unexpected-keyword-arg
hub.update_overview(no_throttle=True)
class VerisureAlarm(alarm.AlarmControlPanel):
"""Representation of a Verisure alarm status."""
def __init__(self, device_id):
"""Initialize the Verisure alarm panel."""
self._id = device_id
def __init__(self):
"""Initalize the Verisure alarm panel."""
self._state = STATE_UNKNOWN
self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None
@@ -41,18 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property
def name(self):
"""Return the name of the device."""
return 'Alarm {}'.format(self._id)
return '{} alarm'.format(hub.session.installations[0]['alias'])
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def available(self):
"""Return True if entity is available."""
return hub.available
@property
def code_format(self):
"""Return the code format as regex."""
@@ -65,33 +70,26 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def update(self):
"""Update alarm status."""
hub.update_alarms()
if hub.alarm_status[self._id].status == 'unarmed':
hub.update_overview()
status = hub.get_first("$.armState.statusType")
if status == 'DISARMED':
self._state = STATE_ALARM_DISARMED
elif hub.alarm_status[self._id].status == 'armedhome':
elif status == 'ARMED_HOME':
self._state = STATE_ALARM_ARMED_HOME
elif hub.alarm_status[self._id].status == 'armed':
elif status == 'ARMED_AWAY':
self._state = STATE_ALARM_ARMED_AWAY
elif hub.alarm_status[self._id].status != 'pending':
_LOGGER.error(
"Unknown alarm state %s", hub.alarm_status[self._id].status)
self._changed_by = hub.alarm_status[self._id].name
elif status != 'PENDING':
_LOGGER.error('Unknown alarm state %s', status)
self._changed_by = hub.get_first("$.armState.name")
def alarm_disarm(self, code=None):
"""Send disarm command."""
hub.my_pages.alarm.set(code, 'DISARMED')
_LOGGER.info("Verisure alarm disarming")
hub.my_pages.alarm.wait_while_pending()
set_arm_state('DISARMED', code)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
hub.my_pages.alarm.set(code, 'ARMED_HOME')
_LOGGER.info("Verisure alarm arming home")
hub.my_pages.alarm.wait_while_pending()
set_arm_state('ARMED_HOME', code)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
_LOGGER.info("Verisure alarm arming away")
hub.my_pages.alarm.wait_while_pending()
set_arm_state('ARMED_AWAY', code)
+22 -6
View File
@@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'alert'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
CONF_DONE_MESSAGE = 'done_message'
CONF_CAN_ACK = 'can_acknowledge'
CONF_NOTIFIERS = 'notifiers'
CONF_REPEAT = 'repeat'
@@ -35,6 +36,7 @@ DEFAULT_SKIP_FIRST = False
ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_DONE_MESSAGE, default=None): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
@@ -121,10 +123,10 @@ def async_setup(hass, config):
# Setup alerts
for entity_id, alert in alerts.items():
entity = Alert(hass, entity_id,
alert[CONF_NAME], alert[CONF_ENTITY_ID],
alert[CONF_STATE], alert[CONF_REPEAT],
alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS],
alert[CONF_CAN_ACK])
alert[CONF_NAME], alert[CONF_DONE_MESSAGE],
alert[CONF_ENTITY_ID], alert[CONF_STATE],
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
all_alerts[entity.entity_id] = entity
# Read descriptions
@@ -154,8 +156,8 @@ def async_setup(hass, config):
class Alert(ToggleEntity):
"""Representation of an alert."""
def __init__(self, hass, entity_id, name, watched_entity_id, state,
repeat, skip_first, notifiers, can_ack):
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
state, repeat, skip_first, notifiers, can_ack):
"""Initialize the alert."""
self.hass = hass
self._name = name
@@ -163,6 +165,7 @@ class Alert(ToggleEntity):
self._skip_first = skip_first
self._notifiers = notifiers
self._can_ack = can_ack
self._done_message = done_message
self._delay = [timedelta(minutes=val) for val in repeat]
self._next_delay = 0
@@ -170,6 +173,7 @@ class Alert(ToggleEntity):
self._firing = False
self._ack = False
self._cancel = None
self._send_done_message = False
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
event.async_track_state_change(
@@ -230,6 +234,8 @@ class Alert(ToggleEntity):
self._cancel()
self._ack = False
self._firing = False
if self._done_message and self._send_done_message:
yield from self._notify_done_message()
self.hass.async_add_job(self.async_update_ha_state)
@asyncio.coroutine
@@ -249,11 +255,21 @@ class Alert(ToggleEntity):
if not self._ack:
_LOGGER.info("Alerting: %s", self._name)
self._send_done_message = True
for target in self._notifiers:
yield from self.hass.services.async_call(
'notify', target, {'message': self._name})
yield from self._schedule_notify()
@asyncio.coroutine
def _notify_done_message(self, *args):
"""Send notification of complete alert."""
_LOGGER.info("Alerting: %s", self._done_message)
self._send_done_message = False
for target in self._notifiers:
yield from self.hass.services.async_call(
'notify', target, {'message': self._done_message})
@asyncio.coroutine
def async_turn_on(self):
"""Async Unacknowledge alert."""
+10 -10
View File
@@ -1,27 +1,27 @@
"""
This component provides basic support for Netgear Arlo IP cameras.
For more details about this platform, please refer to the documentation at
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/arlo/
"""
import logging
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.loader as loader
from requests.exceptions import HTTPError, ConnectTimeout
import homeassistant.loader as loader
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
REQUIREMENTS = ['pyarlo==0.0.4']
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com'
DOMAIN = 'arlo'
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
DATA_ARLO = 'data_arlo'
DEFAULT_BRAND = 'Netgear Arlo'
DOMAIN = 'arlo'
NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Camera Setup'
@@ -47,7 +47,7 @@ def setup(hass, config):
arlo = PyArlo(username, password, preload=False)
if not arlo.is_connected:
return False
hass.data['arlo'] = arlo
hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
persistent_notification.create(
+65 -6
View File
@@ -11,6 +11,7 @@ import os
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
CONF_HOST, CONF_INCLUDE, CONF_NAME,
CONF_PASSWORD, CONF_TRIGGER_TIME,
@@ -18,11 +19,12 @@ from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
from homeassistant.components.discovery import SERVICE_AXIS
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['axis==7']
REQUIREMENTS = ['axis==8']
_LOGGER = logging.getLogger(__name__)
@@ -59,6 +61,21 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
SERVICE_VAPIX_CALL = 'vapix_call'
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
SERVICE_CGI = 'cgi'
SERVICE_ACTION = 'action'
SERVICE_PARAM = 'param'
SERVICE_DEFAULT_CGI = 'param.cgi'
SERVICE_DEFAULT_ACTION = 'update'
SERVICE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(SERVICE_PARAM): cv.string,
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
})
def request_configuration(hass, name, host, serialnumber):
"""Request configuration steps from the user."""
@@ -135,23 +152,34 @@ def setup(hass, base_config):
def axis_device_discovered(service, discovery_info):
"""Called when axis devices has been found."""
host = discovery_info['host']
host = discovery_info[CONF_HOST]
name = discovery_info['hostname']
serialnumber = discovery_info['properties']['macaddress']
if serialnumber not in AXIS_DEVICES:
config_file = _read_config(hass)
if serialnumber in config_file:
# Device config saved to file
try:
config = DEVICE_SCHEMA(config_file[serialnumber])
config[CONF_HOST] = host
except vol.Invalid as err:
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
return False
if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name'])
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
else:
# New device, create configuration request for UI
request_configuration(hass, name, host, serialnumber)
else:
# Device already registered, but on a different IP
device = AXIS_DEVICES[serialnumber]
device.url = host
async_dispatcher_send(hass,
DOMAIN + '_' + device.name + '_new_ip',
host)
# Register discovery service
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
if DOMAIN in base_config:
@@ -160,7 +188,30 @@ def setup(hass, base_config):
if CONF_NAME not in config:
config[CONF_NAME] = device
if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name'])
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
# Services to communicate with device.
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def vapix_service(call):
"""Service to send a message."""
for _, device in AXIS_DEVICES.items():
if device.name == call.data[CONF_NAME]:
response = device.do_request(call.data[SERVICE_CGI],
call.data[SERVICE_ACTION],
call.data[SERVICE_PARAM])
hass.bus.async_fire(SERVICE_VAPIX_CALL_RESPONSE, response)
return True
_LOGGER.info("Couldn\'t find device %s", call.data[CONF_NAME])
return False
# Register service with Home Assistant.
hass.services.register(DOMAIN,
SERVICE_VAPIX_CALL,
vapix_service,
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
schema=SERVICE_SCHEMA)
return True
@@ -190,8 +241,16 @@ def setup_device(hass, config):
if enable_metadatastream:
device.initialize_new_event = event_initialized
device.initiate_metadatastream()
if not device.initiate_metadatastream():
notification = get_component('persistent_notification')
notification.create(hass,
'Dependency missing for sensors, '
'please check documentation',
title=DOMAIN,
notification_id='axis_notification')
AXIS_DEVICES[device.serial_number] = device
return True
@@ -311,4 +370,4 @@ REMAP = [{'type': 'motion',
'class': 'input',
'topic': 'tns1:Device/tnsaxis:IO/Port',
'subscribe': 'onvif:Device/axis:IO/Port',
'platform': 'sensor'}, ]
'platform': 'binary_sensor'}, ]
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ArestBinarySensor(
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
device_class, pin)])
device_class, pin)], True)
class ArestBinarySensor(BinarySensorDevice):
@@ -64,7 +64,6 @@ class ArestBinarySensor(BinarySensorDevice):
self._name = name
self._device_class = device_class
self._pin = pin
self.update()
if self._pin is not None:
request = requests.get(
@@ -8,19 +8,18 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.digital_ocean import (
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
ATTR_REGION, ATTR_VCPUS)
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Droplet'
DEFAULT_SENSOR_CLASS = 'motion'
DEFAULT_SENSOR_CLASS = 'moving'
DEPENDENCIES = ['digital_ocean']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -30,19 +29,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Digital Ocean droplet sensor."""
digital_ocean = get_component('digital_ocean')
digital = hass.data.get(DATA_DIGITAL_OCEAN)
if not digital:
return False
droplets = config.get(CONF_DROPLETS)
dev = []
for droplet in droplets:
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
droplet_id = digital.get_droplet_id(droplet)
if droplet_id is None:
_LOGGER.error("Droplet %s is not available", droplet)
return False
dev.append(DigitalOceanBinarySensor(
digital_ocean.DIGITAL_OCEAN, droplet_id))
dev.append(DigitalOceanBinarySensor(digital, droplet_id))
add_devices(dev)
add_devices(dev, True)
class DigitalOceanBinarySensor(BinarySensorDevice):
@@ -53,7 +54,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
self._digital_ocean = do
self._droplet_id = droplet_id
self._state = None
self.update()
self.data = None
@property
def name(self):
@@ -50,6 +50,10 @@ class ModbusCoilSensor(BinarySensorDevice):
self._coil = int(coil)
self._value = None
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return the state of the sensor."""
@@ -0,0 +1,233 @@
"""
Support for RFXtrx binary sensors.
Lighting4 devices (sensors based on PT2262 encoder) are supported and
tested. Other types may need some work.
"""
import logging
import voluptuous as vol
from homeassistant.components import rfxtrx
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import event as evt
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rfxtrx import (
ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT,
ATTR_DATA_BITS, CONF_DEVICES
)
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
)
DEPENDENCIES = ["rfxtrx"]
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): rfxtrx.DOMAIN,
vol.Optional(CONF_DEVICES, default={}): vol.All(
dict, rfxtrx.valid_binary_sensor),
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
}, extra=vol.ALLOW_EXTRA)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Binary Sensor platform to rfxtrx."""
import RFXtrx as rfxtrxmod
sensors = []
for packet_id, entity in config['devices'].items():
event = rfxtrx.get_rfx_object(packet_id)
device_id = slugify(event.device.id_string.lower())
if device_id in rfxtrx.RFX_DEVICES:
continue
if entity[ATTR_DATA_BITS] is not None:
_LOGGER.info("Masked device id: %s",
rfxtrx.get_pt2262_deviceid(device_id,
entity[ATTR_DATA_BITS]))
_LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)",
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
device = RfxtrxBinarySensor(event, entity[ATTR_NAME],
entity[CONF_DEVICE_CLASS],
entity[ATTR_FIREEVENT],
entity[ATTR_OFF_DELAY],
entity[ATTR_DATA_BITS],
entity[CONF_COMMAND_ON],
entity[CONF_COMMAND_OFF])
device.hass = hass
sensors.append(device)
rfxtrx.RFX_DEVICES[device_id] = device
add_devices_callback(sensors)
# pylint: disable=too-many-branches
def binary_sensor_update(event):
"""Callback for control updates from the RFXtrx gateway."""
if not isinstance(event, rfxtrxmod.ControlEvent):
return
device_id = slugify(event.device.id_string.lower())
if device_id in rfxtrx.RFX_DEVICES:
sensor = rfxtrx.RFX_DEVICES[device_id]
else:
sensor = rfxtrx.get_pt2262_device(device_id)
if sensor is None:
# Add the entity if not exists and automatic_add is True
if not config[ATTR_AUTOMATIC_ADD]:
return
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
if poss_dev is not None:
poss_id = slugify(poss_dev.event.device.id_string.lower())
_LOGGER.info("Found possible matching deviceid %s.",
poss_id)
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
sensor = RfxtrxBinarySensor(event, pkt_id)
rfxtrx.RFX_DEVICES[device_id] = sensor
add_devices_callback([sensor])
_LOGGER.info("Added binary sensor %s "
"(Device_id: %s Class: %s Sub: %s)",
pkt_id,
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
elif not isinstance(sensor, RfxtrxBinarySensor):
return
else:
_LOGGER.info("Binary sensor update "
"(Device_id: %s Class: %s Sub: %s)",
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
if sensor.is_pt2262:
cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits)
_LOGGER.info("applying cmd %s to device_id: %s)",
cmd, sensor.masked_id)
sensor.apply_cmd(int(cmd, 16))
else:
rfxtrx.apply_received_command(event)
if (sensor.is_on and sensor.off_delay is not None and
sensor.delay_listener is None):
def off_delay_listener(now):
"""Switch device off after a delay."""
sensor.delay_listener = None
sensor.update_state(False)
sensor.delay_listener = evt.track_point_in_time(
hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay
)
# Subscribe to main rfxtrx events
if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update)
# pylint: disable=too-many-instance-attributes,too-many-arguments
class RfxtrxBinarySensor(BinarySensorDevice):
"""An Rfxtrx binary sensor."""
def __init__(self, event, name, device_class=None,
should_fire=False, off_delay=None, data_bits=None,
cmd_on=None, cmd_off=None):
"""Initialize the sensor."""
self.event = event
self._name = name
self._should_fire_event = should_fire
self._device_class = device_class
self._off_delay = off_delay
self._state = False
self.delay_listener = None
self._data_bits = data_bits
self._cmd_on = cmd_on
self._cmd_off = cmd_off
if data_bits is not None:
self._masked_id = rfxtrx.get_pt2262_deviceid(
event.device.id_string.lower(),
data_bits)
def __str__(self):
"""Return the name of the sensor."""
return self._name
@property
def name(self):
"""Return the device name."""
return self._name
@property
def is_pt2262(self):
"""Return true if the device is PT2262-based."""
return self._data_bits is not None
@property
def masked_id(self):
"""Return the masked device id (isolated address bits)."""
return self._masked_id
@property
def data_bits(self):
"""Return the number of data bits."""
return self._data_bits
@property
def cmd_on(self):
"""Return the value of the 'On' command."""
return self._cmd_on
@property
def cmd_off(self):
"""Return the value of the 'Off' command."""
return self._cmd_off
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def should_fire_event(self):
"""Return is the device must fire event."""
return self._should_fire_event
@property
def device_class(self):
"""Return the sensor class."""
return self._device_class
@property
def off_delay(self):
"""Return the off_delay attribute value."""
return self._off_delay
@property
def is_on(self):
"""Return true if the sensor state is True."""
return self._state
def apply_cmd(self, cmd):
"""Apply a command for updating the state."""
if cmd == self.cmd_on:
self.update_state(True)
elif cmd == self.cmd_off:
self.update_state(False)
def update_state(self, state):
"""Update the state of the device."""
self._state = state
self.schedule_update_ha_state()
@@ -0,0 +1,59 @@
"""
Interfaces with Verisure sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.verisure/
"""
import logging
from homeassistant.components.verisure import HUB as hub
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.verisure import CONF_DOOR_WINDOW
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Verisure binary sensors."""
sensors = []
hub.update_overview()
if int(hub.config.get(CONF_DOOR_WINDOW, 1)):
sensors.extend([
VerisureDoorWindowSensor(device_label)
for device_label in hub.get(
"$.doorWindow.doorWindowDevice[*].deviceLabel")])
add_devices(sensors)
class VerisureDoorWindowSensor(BinarySensorDevice):
"""Verisure door window sensor."""
def __init__(self, device_label):
"""Initialize the modbus coil sensor."""
self._device_label = device_label
@property
def name(self):
"""Return the name of the binary sensor."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area",
self._device_label)
@property
def is_on(self):
"""Return the state of the sensor."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state",
self._device_label) == "OPEN"
@property
def available(self):
"""Return True if entity is available."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
self._device_label) is not None
def update(self):
"""Update the state of the sensor."""
hub.update_overview()
+1 -1
View File
@@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
if disc_info is None:
return
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]):
return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
+96 -6
View File
@@ -12,13 +12,16 @@ from datetime import timedelta
import logging
import hashlib
from random import SystemRandom
import os
import aiohttp
from aiohttp import web
import async_timeout
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
@@ -26,9 +29,12 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.helpers.event import async_track_time_interval
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
SERVICE_EN_MOTION = 'enable_motion_detection'
SERVICE_DISEN_MOTION = 'disable_motion_detection'
DOMAIN = 'camera'
DEPENDENCIES = ['http']
SCAN_INTERVAL = timedelta(seconds=30)
@@ -38,11 +44,30 @@ STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
DEFAULT_CONTENT_TYPE = 'image/jpeg'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom()
CAMERA_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
def enable_motion_detection(hass, entity_id=None):
"""Enable Motion Detection."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_EN_MOTION, data))
def disable_motion_detection(hass, entity_id=None):
"""Disable Motion Detection."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_DISEN_MOTION, data))
@asyncio.coroutine
def async_get_image(hass, entity_id, timeout=10):
@@ -92,6 +117,44 @@ def async_setup(hass, config):
hass.async_add_job(entity.async_update_ha_state())
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
@asyncio.coroutine
def async_handle_camera_service(service):
"""Handle calls to the camera services."""
target_cameras = component.async_extract_from_service(service)
for camera in target_cameras:
if service.service == SERVICE_EN_MOTION:
yield from camera.async_enable_motion_detection()
elif service.service == SERVICE_DISEN_MOTION:
yield from camera.async_disable_motion_detection()
update_tasks = []
for camera in target_cameras:
if not camera.should_poll:
continue
update_coro = hass.async_add_job(
camera.async_update_ha_state(True))
if hasattr(camera, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks:
yield from asyncio.wait(update_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_EN_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_EN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_DISEN_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_DISEN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
return True
@@ -101,6 +164,7 @@ class Camera(Entity):
def __init__(self):
"""Initialize a camera."""
self.is_streaming = False
self.content_type = DEFAULT_CONTENT_TYPE
self.access_tokens = collections.deque([], 2)
self.async_update_token()
@@ -124,6 +188,11 @@ class Camera(Entity):
"""Return the camera brand."""
return None
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return None
@property
def model(self):
"""Return the camera model."""
@@ -149,16 +218,17 @@ class Camera(Entity):
response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
'boundary=--frameboundary')
yield from response.prepare(request)
def write(img_bytes):
"""Write image to stream."""
response.write(bytes(
'--jpegboundary\r\n'
'Content-Type: image/jpeg\r\n'
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
self.content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
last_image = None
@@ -199,6 +269,22 @@ class Camera(Entity):
else:
return STATE_IDLE
def enable_motion_detection(self):
"""Enable motion detection in the camera."""
raise NotImplementedError()
def async_enable_motion_detection(self):
"""Call the job and enable motion detection."""
return self.hass.async_add_job(self.enable_motion_detection)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
raise NotImplementedError()
def async_disable_motion_detection(self):
"""Call the job and disable motion detection."""
return self.hass.async_add_job(self.disable_motion_detection)
@property
def state_attributes(self):
"""Return the camera state attributes."""
@@ -212,6 +298,9 @@ class Camera(Entity):
if self.brand:
attr['brand'] = self.brand
if self.motion_detection_enabled:
attr['motion_detection'] = self.motion_detection_enabled
return attr
@callback
@@ -269,7 +358,8 @@ class CameraImageView(CameraView):
image = yield from camera.async_camera_image()
if image:
return web.Response(body=image, content_type='image/jpeg')
return web.Response(body=image,
content_type=camera.content_type)
return web.Response(status=500)
+35 -11
View File
@@ -6,32 +6,32 @@ https://home-assistant.io/components/camera.arlo/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.components.arlo import DEFAULT_BRAND
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream)
DEPENDENCIES = ['arlo', 'ffmpeg']
_LOGGER = logging.getLogger(__name__)
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS):
cv.string,
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up an Arlo IP Camera."""
arlo = hass.data.get('arlo')
arlo = hass.data.get(DATA_ARLO)
if not arlo:
return False
@@ -40,7 +40,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
cameras.append(ArloCam(hass, camera, config))
async_add_devices(cameras, True)
return True
class ArloCam(Camera):
@@ -49,14 +48,15 @@ class ArloCam(Camera):
def __init__(self, hass, camera, device_info):
"""Initialize an Arlo camera."""
super().__init__()
self._camera = camera
self._base_stn = hass.data['arlo'].base_stations[0]
self._name = self._camera.name
self._motion_status = False
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
def camera_image(self):
"""Return a still image reponse from the camera."""
"""Return a still image response from the camera."""
return self._camera.last_image
@asyncio.coroutine
@@ -90,3 +90,27 @@ class ArloCam(Camera):
def brand(self):
"""Camera brand."""
return DEFAULT_BRAND
@property
def should_poll(self):
"""Camera should poll periodically."""
return True
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
return self._motion_status
def set_base_station_mode(self, mode):
"""Set the mode in the base station."""
self._base_stn.mode = mode
def enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm)."""
self._motion_status = True
self.set_base_station_mode(ARLO_MODE_ARMED)
def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False
self.set_base_station_mode(ARLO_MODE_DISARMED)
+27 -9
View File
@@ -7,15 +7,16 @@ https://home-assistant.io/components/camera.axis/
import logging
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['axis']
DOMAIN = 'axis'
DEPENDENCIES = [DOMAIN]
def _get_image_url(host, mode):
@@ -27,12 +28,29 @@ def _get_image_url(host, mode):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Axis camera."""
device_info = {
CONF_NAME: discovery_info['name'],
CONF_USERNAME: discovery_info['username'],
CONF_PASSWORD: discovery_info['password'],
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'),
config = {
CONF_NAME: discovery_info[CONF_NAME],
CONF_USERNAME: discovery_info[CONF_USERNAME],
CONF_PASSWORD: discovery_info[CONF_PASSWORD],
CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST], 'mjpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST],
'single'),
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
}
add_devices([MjpegCamera(hass, device_info)])
add_devices([AxisCamera(hass, config)])
class AxisCamera(MjpegCamera):
"""AxisCamera class."""
def __init__(self, hass, config):
"""Initialize Axis Communications camera component."""
super().__init__(hass, config)
async_dispatcher_connect(hass,
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
self._new_ip)
def _new_ip(self, host):
"""Set new IP for video stream."""
self._mjpeg_url = _get_image_url(host, 'mjpeg')
self._still_image_url = _get_image_url(host, 'mjpeg')
+25 -3
View File
@@ -5,25 +5,29 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import os
import logging
import homeassistant.util.dt as dt_util
from homeassistant.components.camera import Camera
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo camera platform."""
add_devices([
DemoCamera('Demo camera')
DemoCamera(hass, config, 'Demo camera')
])
class DemoCamera(Camera):
"""The representation of a Demo camera."""
def __init__(self, name):
def __init__(self, hass, config, name):
"""Initialize demo camera component."""
super().__init__()
self._parent = hass
self._name = name
self._motion_status = False
def camera_image(self):
"""Return a faked still image response."""
@@ -38,3 +42,21 @@ class DemoCamera(Camera):
def name(self):
"""Return the name of this camera."""
return self._name
@property
def should_poll(self):
"""Camera should poll periodically."""
return True
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
return self._motion_status
def enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm)."""
self._motion_status = True
def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False
+5 -1
View File
@@ -17,13 +17,15 @@ from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.components.camera import (
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
CONF_CONTENT_TYPE = 'content_type'
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
CONF_STILL_IMAGE_URL = 'still_image_url'
@@ -37,6 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
})
@@ -59,6 +62,7 @@ class GenericCamera(Camera):
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self.content_type = device_info[CONF_CONTENT_TYPE]
username = device_info.get(CONF_USERNAME)
password = device_info.get(CONF_PASSWORD)
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.local_file/
"""
import logging
import mimetypes
import os
import voluptuous as vol
@@ -46,6 +47,10 @@ class LocalFile(Camera):
self._name = name
self._file_path = file_path
# Set content type of local file
content, _ = mimetypes.guess_type(file_path)
if content is not None:
self.content_type = content
def camera_image(self):
"""Return image response."""
@@ -0,0 +1,17 @@
# Describes the format for available camera services
enable_motion_detection:
description: Enable the motion detection in a camera
fields:
entity_id:
description: Name(s) of entities to enable motion detection
example: 'camera.living_room_camera'
disable_motion_detection:
description: Disable the motion detection in a camera
fields:
entity_id:
description: Name(s) of entities to disable motion detection
example: 'camera.living_room_camera'
+20 -18
View File
@@ -24,22 +24,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if not os.access(directory_path, os.R_OK):
_LOGGER.error("file path %s is not readable", directory_path)
return False
hub.update_smartcam()
hub.update_overview()
smartcams = []
smartcams.extend([
VerisureSmartcam(hass, value.deviceLabel, directory_path)
for value in hub.smartcam_status.values()])
VerisureSmartcam(hass, device_label, directory_path)
for device_label in hub.get(
"$.customerImageCameras[*].deviceLabel")])
add_devices(smartcams)
class VerisureSmartcam(Camera):
"""Representation of a Verisure camera."""
def __init__(self, hass, device_id, directory_path):
def __init__(self, hass, device_label, directory_path):
"""Initialize Verisure File Camera component."""
super().__init__()
self._device_id = device_id
self._device_label = device_label
self._directory_path = directory_path
self._image = None
self._image_id = None
@@ -58,28 +59,27 @@ class VerisureSmartcam(Camera):
def check_imagelist(self):
"""Check the contents of the image list."""
hub.update_smartcam_imagelist()
if (self._device_id not in hub.smartcam_dict or
not hub.smartcam_dict[self._device_id]):
hub.update_smartcam_imageseries()
image_ids = hub.get_image_info(
"$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId",
self._device_label)
if not image_ids:
return
images = hub.smartcam_dict[self._device_id]
new_image_id = images[0]
_LOGGER.debug("self._device_id=%s, self._images=%s, "
"self._new_image_id=%s", self._device_id,
images, new_image_id)
new_image_id = image_ids[0]
if (new_image_id == '-1' or
self._image_id == new_image_id):
_LOGGER.debug("The image is the same, or loading image_id")
return
_LOGGER.debug("Download new image %s", new_image_id)
hub.my_pages.smartcam.download_image(
self._device_id, new_image_id, self._directory_path)
new_image_path = os.path.join(
self._directory_path, '{}{}'.format(new_image_id, '.jpg'))
hub.session.download_image(
self._device_label, new_image_id, new_image_path)
_LOGGER.debug("Old image_id=%s", self._image_id)
self.delete_image(self)
self._image_id = new_image_id
self._image = os.path.join(
self._directory_path, '{}{}'.format(self._image_id, '.jpg'))
self._image = new_image_path
def delete_image(self, event):
"""Delete an old image."""
@@ -95,4 +95,6 @@ class VerisureSmartcam(Camera):
@property
def name(self):
"""Return the name of this camera."""
return hub.smartcam_status[self._device_id].location
return hub.get_first(
"$.customerImageCameras[?(@.deviceLabel=='%s')].area",
self._device_label)
+7 -1
View File
@@ -693,8 +693,14 @@ class ClimateDevice(Entity):
def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes."""
if temp is None or not isinstance(temp, Number):
if temp is None:
return temp
# if the temperature is not a number this can cause issues
# with polymer components, so bail early there.
if not isinstance(temp, Number):
raise TypeError("Temperature is not a number: %s" % temp)
if self.temperature_unit != self.unit_of_measurement:
temp = convert_temperature(
temp, self.temperature_unit, self.unit_of_measurement)
@@ -67,7 +67,12 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
return self._values.get(self.gateway.const.SetReq.V_TEMP)
value = self._values.get(self.gateway.const.SetReq.V_TEMP)
if value is not None:
value = float(value)
return value
@property
def target_temperature(self):
@@ -79,21 +84,21 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
if temp is None:
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
return temp
return float(temp)
@property
def target_temperature_high(self):
"""Return the highbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_HEAT in self._values:
return self._values.get(set_req.V_HVAC_SETPOINT_COOL)
return float(self._values.get(set_req.V_HVAC_SETPOINT_COOL))
@property
def target_temperature_low(self):
"""Return the lowbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_COOL in self._values:
return self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
return float(self._values.get(set_req.V_HVAC_SETPOINT_HEAT))
@property
def current_operation(self):
+15 -1
View File
@@ -15,7 +15,7 @@ from homeassistant.components.climate import (
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['radiotherm==1.2']
REQUIREMENTS = ['radiotherm==1.3']
_LOGGER = logging.getLogger(__name__)
@@ -84,6 +84,7 @@ class RadioThermostat(ClimateDevice):
self._name = None
self._fmode = None
self._tmode = None
self._tstate = None
self._hold_temp = hold_temp
self._away = False
self._away_temps = away_temps
@@ -140,6 +141,7 @@ class RadioThermostat(ClimateDevice):
self._name = self.device.name['raw']
self._fmode = self.device.fmode['human']
self._tmode = self.device.tmode['human']
self._tstate = self.device.tstate['human']
if self._tmode == 'Cool':
self._target_temperature = self.device.t_cool['raw']
@@ -147,6 +149,12 @@ class RadioThermostat(ClimateDevice):
elif self._tmode == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_HEAT
elif self._tmode == 'Auto':
if self._tstate == 'Cool':
self._target_temperature = self.device.t_cool['raw']
elif self._tstate == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_AUTO
else:
self._current_operation = STATE_IDLE
@@ -159,6 +167,12 @@ class RadioThermostat(ClimateDevice):
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_HEAT:
self.device.t_heat = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_AUTO:
if self._tstate == 'Cool':
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._tstate == 'Heat':
self.device.t_heat = round(temperature * 2.0) / 2.0
if self._hold_temp or self._away:
self.device.hold = 1
else:
+4 -2
View File
@@ -16,6 +16,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.temperature import convert as convert_temperature
@@ -52,9 +53,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)):
if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]:
devices.append(SensiboClimate(client, dev))
except aiohttp.client_exceptions.ClientConnectorError:
except (aiohttp.client_exceptions.ClientConnectorError,
asyncio.TimeoutError):
_LOGGER.exception('Failed to connct to Sensibo servers.')
return False
raise PlatformNotReady
if devices:
async_add_devices(devices)
+81 -33
View File
@@ -24,6 +24,17 @@ CONST_OVERLAY_MANUAL = 'MANUAL'
# the temperature will be reset after a timespan
CONST_OVERLAY_TIMER = 'TIMER'
CONST_MODE_FAN_HIGH = 'HIGH'
CONST_MODE_FAN_MIDDLE = 'MIDDLE'
CONST_MODE_FAN_LOW = 'LOW'
FAN_MODES_LIST = {
CONST_MODE_FAN_HIGH: 'High',
CONST_MODE_FAN_MIDDLE: 'Middle',
CONST_MODE_FAN_LOW: 'Low',
CONST_MODE_OFF: 'Off',
}
OPERATION_LIST = {
CONST_OVERLAY_MANUAL: 'Manual',
CONST_OVERLAY_TIMER: 'Timer',
@@ -60,9 +71,15 @@ def create_climate_device(tado, hass, zone, name, zone_id):
capabilities = tado.get_capabilities(zone_id)
unit = TEMP_CELSIUS
min_temp = float(capabilities['temperatures']['celsius']['min'])
max_temp = float(capabilities['temperatures']['celsius']['max'])
ac_mode = capabilities['type'] != 'HEATING'
ac_mode = capabilities['type'] == 'AIR_CONDITIONING'
if ac_mode:
temperatures = capabilities['HEAT']['temperatures']
else:
temperatures = capabilities['temperatures']
min_temp = float(temperatures['celsius']['min'])
max_temp = float(temperatures['celsius']['max'])
data_id = 'zone {} {}'.format(name, zone_id)
device = TadoClimate(tado,
@@ -107,7 +124,9 @@ class TadoClimate(ClimateDevice):
self._max_temp = max_temp
self._target_temp = None
self._tolerance = tolerance
self._cooling = False
self._current_fan = CONST_MODE_OFF
self._current_operation = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
@@ -129,13 +148,32 @@ class TadoClimate(ClimateDevice):
@property
def current_operation(self):
"""Return current readable operation mode."""
return OPERATION_LIST.get(self._current_operation)
if self._cooling:
return "Cooling"
else:
return OPERATION_LIST.get(self._current_operation)
@property
def operation_list(self):
"""Return the list of available operation modes (readable)."""
return list(OPERATION_LIST.values())
@property
def current_fan_mode(self):
"""Return the fan setting."""
if self.ac_mode:
return FAN_MODES_LIST.get(self._current_fan)
else:
return None
@property
def fan_list(self):
"""List of available fan modes."""
if self.ac_mode:
return list(FAN_MODES_LIST.values())
else:
return None
@property
def temperature_unit(self):
"""Return the unit of measurement used by the platform."""
@@ -205,27 +243,27 @@ class TadoClimate(ClimateDevice):
if 'sensorDataPoints' in data:
sensor_data = data['sensorDataPoints']
temperature = float(
sensor_data['insideTemperature']['celsius'])
humidity = float(
sensor_data['humidity']['percentage'])
setting = 0
unit = TEMP_CELSIUS
if 'insideTemperature' in sensor_data:
temperature = float(
sensor_data['insideTemperature']['celsius'])
self._cur_temp = self.hass.config.units.temperature(
temperature, unit)
if 'humidity' in sensor_data:
humidity = float(
sensor_data['humidity']['percentage'])
self._cur_humidity = humidity
# temperature setting will not exist when device is off
if 'temperature' in data['setting'] and \
data['setting']['temperature'] is not None:
setting = float(
data['setting']['temperature']['celsius'])
unit = TEMP_CELSIUS
self._cur_temp = self.hass.config.units.temperature(
temperature, unit)
self._target_temp = self.hass.config.units.temperature(
setting, unit)
self._cur_humidity = humidity
self._target_temp = self.hass.config.units.temperature(
setting, unit)
if 'tadoMode' in data:
mode = data['tadoMode']
@@ -235,29 +273,39 @@ class TadoClimate(ClimateDevice):
power = data['setting']['power']
if power == 'OFF':
self._current_operation = CONST_MODE_OFF
self._current_fan = CONST_MODE_OFF
# There is no overlay, the mode will always be
# "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._device_is_active = False
else:
self._device_is_active = True
if 'overlay' in data and data['overlay'] is not None:
overlay = True
termination = data['overlay']['termination']['type']
else:
if self._device_is_active:
overlay = False
termination = ""
overlay_data = None
termination = self._current_operation
cooling = False
fan_speed = CONST_MODE_OFF
# If you set mode manualy to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
if 'overlay' in data:
overlay_data = data['overlay']
overlay = overlay_data is not None
if overlay:
termination = overlay_data['termination']['type']
if 'setting' in overlay_data:
cooling = overlay_data['setting']['mode'] == 'COOL'
fan_speed = overlay_data['setting']['fanSpeed']
# If you set mode manualy to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
if overlay and self._device_is_active:
# There is an overlay the device is on
self._overlay_mode = termination
self._current_operation = termination
else:
# There is no overlay, the mode will always be
# "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._current_operation = CONST_MODE_SMART_SCHEDULE
self._cooling = cooling
self._current_fan = fan_speed
def _control_heating(self):
"""Send new target temperature to mytado."""
+134
View File
@@ -0,0 +1,134 @@
"""
Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/comfoconnect/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_TOKEN, CONF_PIN, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import (discovery)
from homeassistant.helpers.dispatcher import (dispatcher_send)
REQUIREMENTS = ['pycomfoconnect==0.3']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'comfoconnect'
SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received'
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
ATTR_CURRENT_HUMIDITY = 'current_humidity'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
ATTR_OUTSIDE_HUMIDITY = 'outside_humidity'
ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply'
ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust'
CONF_USER_AGENT = 'user_agent'
DEFAULT_NAME = 'ComfoAirQ'
DEFAULT_PIN = 0
DEFAULT_TOKEN = '00000000000000000000000000000001'
DEFAULT_USER_AGENT = 'Home Assistant'
DEVICE = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN):
vol.Length(min=32, max=32, msg='invalid token'),
vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the ComfoConnect bridge."""
from pycomfoconnect import (Bridge)
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
name = conf.get(CONF_NAME)
token = conf.get(CONF_TOKEN)
user_agent = conf.get(CONF_USER_AGENT)
pin = conf.get(CONF_PIN)
# Run discovery on the configured ip
bridges = Bridge.discover(host)
if not bridges:
_LOGGER.error("Could not connect to ComfoConnect bridge on %s", host)
return False
bridge = bridges[0]
_LOGGER.info("Bridge found: %s (%s)", bridge.uuid.hex(), bridge.host)
# Setup ComfoConnect Bridge
ccb = ComfoConnectBridge(hass, bridge, name, token, user_agent, pin)
hass.data[DOMAIN] = ccb
# Start connection with bridge
ccb.connect()
# Schedule disconnect on shutdown
def _shutdown(_event):
ccb.disconnect()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
# Load platforms
discovery.load_platform(hass, 'fan', DOMAIN, {}, config)
return True
class ComfoConnectBridge(object):
"""Representation of a ComfoConnect bridge."""
def __init__(self, hass, bridge, name, token, friendly_name, pin):
"""Initialize the ComfoConnect bridge."""
from pycomfoconnect import (ComfoConnect)
self.data = {}
self.name = name
self.hass = hass
self.comfoconnect = ComfoConnect(
bridge=bridge, local_uuid=bytes.fromhex(token),
local_devicename=friendly_name, pin=pin)
self.comfoconnect.callback_sensor = self.sensor_callback
def connect(self):
"""Connect with the bridge."""
_LOGGER.debug("Connecting with bridge")
self.comfoconnect.connect(True)
def disconnect(self):
"""Disconnect from the bridge."""
_LOGGER.debug("Disconnecting from bridge")
self.comfoconnect.disconnect()
def sensor_callback(self, var, value):
"""Callback function for sensor updates."""
_LOGGER.debug("Got value from bridge: %d = %d", var, value)
from pycomfoconnect import (
SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR)
if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]:
self.data[var] = value / 10
else:
self.data[var] = value
# Notify listeners that we have received an update
dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var)
def subscribe_sensor(self, sensor_id):
"""Subscribe for the specified sensor."""
self.comfoconnect.register_sensor(sensor_id)
@@ -40,6 +40,8 @@ DEVICE_CLASSES = [
'garage', # Garage door control
]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
SUPPORT_OPEN = 1
SUPPORT_CLOSE = 2
SUPPORT_SET_POSITION = 4
+185
View File
@@ -0,0 +1,185 @@
"""
Support for KNX covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.knx/
"""
import logging
import voluptuous as vol
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)
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_INVERT_POSITION = 'invert_position'
CONF_INVERT_ANGLE = 'invert_angle'
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_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))])
class KNXCover(KNXMultiAddressDevice, CoverDevice):
"""Representation of a KNX cover. e.g. a rollershutter."""
def __init__(self, hass, config):
"""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
# 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
@property
def should_poll(self):
"""Polling is needed for the KNX cover."""
return True
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
@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
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._current_pos
@property
def target_position(self):
"""Return the position we are trying to reach: 0 - 100."""
return self._target_pos
@property
def current_cover_tilt_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._current_tilt
@property
def target_tilt(self):
"""Return the tilt angle (in %) we are trying to reach: 0 - 100."""
return self._target_tilt
def set_cover_position(self, **kwargs):
"""Set new target position."""
position = kwargs.get(ATTR_POSITION)
if position is None:
return
if self._invert_position:
position = 100-position
self._target_pos = position
self.set_percentage('setposition', position)
_LOGGER.debug("%s: Set target position to %d", self.name, position)
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)
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
+354
View File
@@ -0,0 +1,354 @@
"""
Support for covers which integrate with other components.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.template/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.cover import (
ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA,
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT,
SUPPORT_SET_TILT_POSITION, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION)
from homeassistant.const import (
CONF_FRIENDLY_NAME, CONF_ENTITY_ID,
EVENT_HOMEASSISTANT_START, MATCH_ALL,
CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE,
STATE_OPEN, STATE_CLOSED)
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.restore_state import async_get_last_state
from homeassistant.helpers.script import Script
_LOGGER = logging.getLogger(__name__)
_VALID_STATES = [STATE_OPEN, STATE_CLOSED, 'true', 'false']
CONF_COVERS = 'covers'
CONF_POSITION_TEMPLATE = 'position_template'
CONF_TILT_TEMPLATE = 'tilt_template'
OPEN_ACTION = 'open_cover'
CLOSE_ACTION = 'close_cover'
STOP_ACTION = 'stop_cover'
POSITION_ACTION = 'set_cover_position'
TILT_ACTION = 'set_cover_tilt_position'
CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position'
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
SUPPORT_SET_TILT_POSITION)
COVER_SCHEMA = vol.Schema({
vol.Required(OPEN_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CLOSE_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(STOP_ACTION): cv.SCRIPT_SCHEMA,
vol.Exclusive(CONF_POSITION_TEMPLATE,
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
vol.Exclusive(CONF_VALUE_TEMPLATE,
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_TILT_TEMPLATE): cv.template,
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Template cover."""
covers = []
for device, device_config in config[CONF_COVERS].items():
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
state_template = device_config.get(CONF_VALUE_TEMPLATE)
position_template = device_config.get(CONF_POSITION_TEMPLATE)
tilt_template = device_config.get(CONF_TILT_TEMPLATE)
icon_template = device_config.get(CONF_ICON_TEMPLATE)
open_action = device_config[OPEN_ACTION]
close_action = device_config[CLOSE_ACTION]
stop_action = device_config[STOP_ACTION]
position_action = device_config.get(POSITION_ACTION)
tilt_action = device_config.get(TILT_ACTION)
if position_template is None and state_template is None:
_LOGGER.error('Must specify either %s' or '%s',
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE)
continue
template_entity_ids = set()
if state_template is not None:
temp_ids = state_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if position_template is not None:
temp_ids = position_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if tilt_template is not None:
temp_ids = tilt_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if icon_template is not None:
temp_ids = icon_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if not template_entity_ids:
template_entity_ids = MATCH_ALL
entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids)
covers.append(
CoverTemplate(
hass,
device, friendly_name, state_template,
position_template, tilt_template, icon_template,
open_action, close_action, stop_action,
position_action, tilt_action, entity_ids
)
)
if not covers:
_LOGGER.error("No covers added")
return False
async_add_devices(covers, True)
return True
class CoverTemplate(CoverDevice):
"""Representation of a Template cover."""
def __init__(self, hass, device_id, friendly_name, state_template,
position_template, tilt_template, icon_template,
open_action, close_action, stop_action,
position_action, tilt_action, entity_ids):
"""Initialize the Template cover."""
self.hass = hass
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, device_id, hass=hass)
self._name = friendly_name
self._template = state_template
self._position_template = position_template
self._tilt_template = tilt_template
self._icon_template = icon_template
self._open_script = Script(hass, open_action)
self._close_script = Script(hass, close_action)
self._stop_script = Script(hass, stop_action)
self._position_script = None
if position_action is not None:
self._position_script = Script(hass, position_action)
self._tilt_script = None
if tilt_action is not None:
self._tilt_script = Script(hass, tilt_action)
self._icon = None
self._position = None
self._tilt_value = None
self._entities = entity_ids
if self._template is not None:
self._template.hass = self.hass
if self._position_template is not None:
self._position_template.hass = self.hass
if self._tilt_template is not None:
self._tilt_template.hass = self.hass
if self._icon_template is not None:
self._icon_template.hass = self.hass
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._position = 100 if state.state == STATE_OPEN else 0
@callback
def template_cover_state_listener(entity, old_state, new_state):
"""Handle target device state changes."""
self.hass.async_add_job(self.async_update_ha_state(True))
@callback
def template_cover_startup(event):
"""Update template on startup."""
async_track_state_change(
self.hass, self._entities, template_cover_state_listener)
self.hass.async_add_job(self.async_update_ha_state(True))
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_cover_startup)
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._position == 0
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._position
@property
def current_cover_tilt_position(self):
"""Return current position of cover tilt.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._tilt_value
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._icon
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
if self.current_cover_position is not None:
supported_features |= SUPPORT_SET_POSITION
if self.current_cover_tilt_position is not None:
supported_features |= TILT_FEATURES
return supported_features
@property
def should_poll(self):
"""Return the polling state."""
return False
@asyncio.coroutine
def async_open_cover(self, **kwargs):
"""Move the cover up."""
self.hass.async_add_job(self._open_script.async_run())
@asyncio.coroutine
def async_close_cover(self, **kwargs):
"""Move the cover down."""
self.hass.async_add_job(self._close_script.async_run())
@asyncio.coroutine
def async_stop_cover(self, **kwargs):
"""Fire the stop action."""
self.hass.async_add_job(self._stop_script.async_run())
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
"""Set cover position."""
if ATTR_POSITION not in kwargs:
return
self._position = kwargs[ATTR_POSITION]
self.hass.async_add_job(self._position_script.async_run(
{"position": self._position}))
@asyncio.coroutine
def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open."""
self._tilt_value = 100
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed."""
self._tilt_value = 0
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
if ATTR_TILT_POSITION not in kwargs:
return
self._tilt_value = kwargs[ATTR_TILT_POSITION]
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_update(self):
"""Update the state from the template."""
if self._template is not None:
try:
state = self._template.async_render().lower()
if state in _VALID_STATES:
if state in ('true', STATE_OPEN):
self._position = 100
else:
self._position = 0
else:
_LOGGER.error(
'Received invalid cover is_on state: %s. Expected: %s',
state, ', '.join(_VALID_STATES))
self._position = None
except TemplateError as ex:
_LOGGER.error(ex)
self._position = None
if self._position_template is not None:
try:
state = float(self._position_template.async_render())
if state < 0 or state > 100:
self._position = None
_LOGGER.error("Cover position value must be"
" between 0 and 100."
" Value was: %.2f", state)
else:
self._position = state
except TemplateError as ex:
_LOGGER.error(ex)
self._position = None
except ValueError as ex:
_LOGGER.error(ex)
self._position = None
if self._tilt_template is not None:
try:
state = float(self._tilt_template.async_render())
if state < 0 or state > 100:
self._tilt_value = None
_LOGGER.error("Tilt value must be between 0 and 100."
" Value was: %.2f", state)
else:
self._tilt_value = state
except TemplateError as ex:
_LOGGER.error(ex)
self._tilt_value = None
except ValueError as ex:
_LOGGER.error(ex)
self._tilt_value = None
if self._icon_template is not None:
try:
self._icon = self._icon_template.async_render()
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning('Could not render icon template %s,'
' the state is unknown.', self._name)
return
self._icon = super().icon
_LOGGER.error('Could not render icon template %s: %s',
self._name, ex)
+1
View File
@@ -210,6 +210,7 @@ def async_setup(hass, config):
description=("Press the button on the bridge to register Philips "
"Hue with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg",
fields=[{'id': 'username', 'name': 'Username'}],
submit_caption="I have pressed the button"
)
configurator_ids.append(request_id)
@@ -0,0 +1,110 @@
"""Support for Linksys Smart Wifi routers."""
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
DEFAULT_TIMEOUT = 10
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
})
def get_scanner(hass, config):
"""Validate the configuration and return a Linksys AP scanner."""
try:
return LinksysSmartWifiDeviceScanner(config[DOMAIN])
except ConnectionError:
return None
class LinksysSmartWifiDeviceScanner(DeviceScanner):
"""This class queries a Linksys Access Point."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.lock = threading.Lock()
self.last_results = {}
# Check if the access point is accessible
response = self._make_request()
if not response.status_code == 200:
raise ConnectionError("Cannot connect to Linksys Access Point")
def scan_devices(self):
"""Scan for new devices and return a list with device IDs (MACs)."""
self._update_info()
return self.last_results.keys()
def get_device_name(self, mac):
"""Return the name (if known) of the device."""
return self.last_results.get(mac)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Check for connected devices."""
with self.lock:
_LOGGER.info("Checking Linksys Smart Wifi")
self.last_results = {}
response = self._make_request()
if response.status_code != 200:
_LOGGER.error(
"Got HTTP status code %d when getting device list",
response.status_code)
return False
try:
data = response.json()
result = data["responses"][0]
devices = result["output"]["devices"]
for device in devices:
macs = device["knownMACAddresses"]
if not macs:
_LOGGER.warning(
"Skipping device without known MAC address")
continue
mac = macs[-1]
connections = device["connections"]
if not connections:
_LOGGER.debug("Device %s is not connected", mac)
continue
name = device["friendlyName"]
properties = device["properties"]
for prop in properties:
if prop["name"] == "userDeviceName":
name = prop["value"]
_LOGGER.debug("Device %s is connected", mac)
self.last_results[mac] = name
except (KeyError, IndexError):
_LOGGER.exception("Router returned unexpected response")
return False
return True
def _make_request(self):
# Weirdly enough, this doesn't seem to require authentication
data = [{
"request": {
"sinceRevision": 0
},
"action": "http://linksys.com/jnap/devicelist/GetDevices"
}]
headers = {"X-JNAP-Action": "http://linksys.com/jnap/core/Transaction"}
return requests.post('http://{}/JNAP/'.format(self.host),
timeout=DEFAULT_TIMEOUT,
headers=headers,
json=data)
@@ -158,6 +158,11 @@ class MikrotikScanner(DeviceScanner):
for device in devices
}
else:
self.last_results = mac_names
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in device_names
if device.get('active-address')
}
return True
@@ -21,7 +21,7 @@ from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
DEPENDENCIES = ['mqtt']
REQUIREMENTS = ['libnacl==1.5.0']
REQUIREMENTS = ['libnacl==1.5.1']
_LOGGER = logging.getLogger(__name__)
+30 -2
View File
@@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
from homeassistant.exceptions import HomeAssistantError
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
@@ -38,6 +39,23 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
def _refresh_on_acccess_denied(func):
"""If remove rebooted, it lost our session so rebuld one and try again."""
def decorator(self, *args, **kwargs):
"""Wrapper function to refresh session_id on PermissionError."""
try:
return func(self, *args, **kwargs)
except PermissionError:
_LOGGER.warning("Invalid session detected." +
" Tryign to refresh session_id and re-run the rpc")
self.session_id = _get_session_id(self.url, self.username,
self.password)
return func(self, *args, **kwargs)
return decorator
class UbusDeviceScanner(DeviceScanner):
"""
This class queries a wireless router running OpenWrt firmware.
@@ -48,14 +66,16 @@ class UbusDeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialize the scanner."""
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/ubus'.format(host)
self.session_id = _get_session_id(self.url, username, password)
self.session_id = _get_session_id(self.url, self.username,
self.password)
self.hostapd = []
self.leasefile = None
self.mac2name = None
@@ -66,6 +86,7 @@ class UbusDeviceScanner(DeviceScanner):
self._update_info()
return self.last_results
@_refresh_on_acccess_denied
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
@@ -95,6 +116,7 @@ class UbusDeviceScanner(DeviceScanner):
return self.mac2name.get(device.upper(), None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
@_refresh_on_acccess_denied
def _update_info(self):
"""Ensure the information from the Luci router is up to date.
@@ -142,6 +164,12 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
if res.status_code == 200:
response = res.json()
if 'error' in response:
if 'message' in response['error'] and \
response['error']['message'] == "Access denied":
raise PermissionError(response['error']['message'])
else:
raise HomeAssistantError(response['error']['message'])
if rpcmethod == "call":
try:
+6 -5
View File
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.11']
REQUIREMENTS = ['python-digitalocean==1.12']
_LOGGER = logging.getLogger(__name__)
@@ -29,7 +29,7 @@ ATTR_VCPUS = 'vcpus'
CONF_DROPLETS = 'droplets'
DIGITAL_OCEAN = None
DATA_DIGITAL_OCEAN = 'data_do'
DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor']
DOMAIN = 'digital_ocean'
@@ -47,13 +47,14 @@ def setup(hass, config):
conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN)
global DIGITAL_OCEAN
DIGITAL_OCEAN = DigitalOcean(access_token)
digital = DigitalOcean(access_token)
if not DIGITAL_OCEAN.manager.get_account():
if not digital.manager.get_account():
_LOGGER.error("No Digital Ocean account found for the given API Token")
return False
hass.data[DATA_DIGITAL_OCEAN] = digital
return True
+1
View File
@@ -55,6 +55,7 @@ SERVICE_HANDLERS = {
'apple_tv': ('media_player', 'apple_tv'),
'frontier_silicon': ('media_player', 'frontier_silicon'),
'openhome': ('media_player', 'openhome'),
'harmony': ('remote', 'harmony'),
'bose_soundtouch': ('media_player', 'soundtouch'),
}
@@ -0,0 +1,118 @@
"""
Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.comfoconnect/
"""
import logging
from homeassistant.components.comfoconnect import (
DOMAIN, ComfoConnectBridge, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED)
from homeassistant.components.fan import (
FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.dispatcher import (dispatcher_connect)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['comfoconnect']
SPEED_MAPPING = {
0: SPEED_OFF,
1: SPEED_LOW,
2: SPEED_MEDIUM,
3: SPEED_HIGH
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the ComfoConnect fan platform."""
ccb = hass.data[DOMAIN]
add_devices([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True)
return
class ComfoConnectFan(FanEntity):
"""Representation of the ComfoConnect fan platform."""
def __init__(self, hass, name, ccb: ComfoConnectBridge):
"""Initialize the ComfoConnect fan."""
from pycomfoconnect import SENSOR_FAN_SPEED_MODE
self._ccb = ccb
self._name = name
# Ask the bridge to keep us updated
self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE)
def _handle_update(var):
if var == SENSOR_FAN_SPEED_MODE:
_LOGGER.debug("Dispatcher update for %s", var)
self.schedule_update_ha_state()
# Register for dispatcher updates
dispatcher_connect(
hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update)
@property
def name(self):
"""Return the name of the fan."""
return self._name
@property
def icon(self):
"""Return the icon to use in the frontend."""
return 'mdi:air-conditioner'
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED
@property
def speed(self):
"""Return the current fan mode."""
from pycomfoconnect import (SENSOR_FAN_SPEED_MODE)
try:
speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
return SPEED_MAPPING[speed]
except KeyError:
return STATE_UNKNOWN
@property
def speed_list(self):
"""List of available fan modes."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def turn_on(self, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
if speed is None:
speed = SPEED_LOW
self.set_speed(speed)
def turn_off(self) -> None:
"""Turn off the fan (to away)."""
self.set_speed(SPEED_OFF)
def set_speed(self, mode):
"""Set fan speed."""
_LOGGER.debug('Changing fan mode to %s.', mode)
from pycomfoconnect import (
CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM,
CMD_FAN_MODE_HIGH)
if mode == SPEED_OFF:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY)
elif mode == SPEED_LOW:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW)
elif mode == SPEED_MEDIUM:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM)
elif mode == SPEED_HIGH:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH)
# Update current mode
self.schedule_update_ha_state()
+1 -1
View File
@@ -75,7 +75,7 @@ class DysonPureCoolLinkDevice(FanEntity):
def async_added_to_hass(self):
"""Callback when entity is added to hass."""
self.hass.async_add_job(
self._device.add_message_listener(self.on_message))
self._device.add_message_listener, self.on_message)
def on_message(self, message):
"""Called when new messages received from the fan."""
@@ -0,0 +1,195 @@
"""
Support for Insteon fans via local hub control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan.insteon_local/
"""
import json
import logging
import os
from datetime import timedelta
from homeassistant.components.fan import (
ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED, FanEntity)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
import homeassistant.util as util
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['insteon_local']
DOMAIN = 'fan'
INSTEON_LOCAL_FANS_CONF = 'insteon_local_fans.conf'
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
SUPPORT_INSTEON_LOCAL = SUPPORT_SET_SPEED
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon local fan platform."""
insteonhub = hass.data['insteon_local']
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if len(conf_fans):
for device_id in conf_fans:
setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
add_devices)
else:
linked = insteonhub.get_linked()
for device_id in linked:
if (linked[device_id]['cat_type'] == 'dimmer' and
linked[device_id]['sku'] == '2475F' and
device_id not in conf_fans):
request_configuration(device_id,
insteonhub,
linked[device_id]['model_name'] + ' ' +
linked[device_id]['sku'],
hass, add_devices)
def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
# We got an error if this method is called while we are configuring
if device_id in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[device_id], 'Failed to register, please try again.')
return
def insteon_fan_config_callback(data):
"""The actions to do when our configuration callback is called."""
setup_fan(device_id, data.get('name'), insteonhub, hass,
add_devices_callback)
_CONFIGURING[device_id] = configurator.request_config(
hass, 'Insteon ' + model + ' addr: ' + device_id,
insteon_fan_config_callback,
description=('Enter a name for ' + model + ' Fan addr: ' + device_id),
entity_picture='/static/images/config_insteon.png',
submit_caption='Confirm',
fields=[{'id': 'name', 'name': 'Name', 'type': ''}]
)
def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
"""Set up the fan."""
if device_id in _CONFIGURING:
request_id = _CONFIGURING.pop(device_id)
configurator = get_component('configurator')
configurator.request_done(request_id)
_LOGGER.info("Device configuration done!")
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if device_id not in conf_fans:
conf_fans[device_id] = name
if not config_from_file(
hass.config.path(INSTEON_LOCAL_FANS_CONF),
conf_fans):
_LOGGER.error("Failed to save configuration file")
device = insteonhub.fan(device_id)
add_devices_callback([InsteonLocalFanDevice(device, name)])
def config_from_file(filename, config=None):
"""Small configuration file management function."""
if config:
# We're writing configuration
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error('Saving config file failed: %s', error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error("Reading configuration file failed: %s", error)
# This won't work yet
return False
else:
return {}
class InsteonLocalFanDevice(FanEntity):
"""An abstract Class for an Insteon node."""
def __init__(self, node, name):
"""Initialize the device."""
self.node = node
self.node.deviceName = name
self._speed = SPEED_OFF
@property
def name(self):
"""Return the the name of the node."""
return self.node.deviceName
@property
def unique_id(self):
"""Return the ID of this Insteon node."""
return 'insteon_local_{}_fan'.format(self.node.device_id)
@property
def speed(self) -> str:
"""Return the current speed."""
return self._speed
@property
def speed_list(self: ToggleEntity) -> list:
"""Get the list of available speeds."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
"""Update state of the fan."""
resp = self.node.status()
if 'cmd2' in resp:
if resp['cmd2'] == '00':
self._speed = SPEED_OFF
elif resp['cmd2'] == '55':
self._speed = SPEED_LOW
elif resp['cmd2'] == 'AA':
self._speed = SPEED_MEDIUM
elif resp['cmd2'] == 'FF':
self._speed = SPEED_HIGH
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_INSTEON_LOCAL
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn device on."""
if speed is None:
if ATTR_SPEED in kwargs:
speed = kwargs[ATTR_SPEED]
else:
speed = SPEED_MEDIUM
self.set_speed(speed)
def turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn device off."""
self.node.off()
def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan."""
if self.node.on(speed):
self._speed = speed
+13 -13
View File
@@ -3,21 +3,21 @@
FINGERPRINTS = {
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
"frontend.html": "cca45decbed803e7f0ec0b4f6e18fe53",
"mdi.html": "1a5ad9654c1f0e57440e30afd92846a5",
"frontend.html": "f170a7221615ca2839cb8fd51a82f50a",
"mdi.html": "c92bd28c434865d6cabb34cd3c0a3e4c",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1",
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
"panels/ha-panel-automation.html": "4f98839bb082885657bbcd0ac04fc680",
"panels/ha-panel-config.html": "76853de505d173e82249bf605eb73505",
"panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61",
"panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
"panels/ha-panel-dev-service.html": "92c6be30b1af95791d5a6429df505852",
"panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869",
"panels/ha-panel-dev-template.html": "d33a55b937b50cdfe8b6fae81f70a139",
"panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229",
"panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1",
"panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
"panels/ha-panel-zwave.html": "92edac58dd52c297c761fd9acec7f436",
"panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
"panels/ha-panel-map.html": "0ba605729197c4724ecc7310b08f7050",
"panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
}
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the Apache 2.0 license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html>
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{white-space:pre-wrap}</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="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the Apache 2.0 license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html>
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
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8 -5
View File
@@ -58,6 +58,9 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
RE_DIGIT_TAIL = re.compile(r'^[^\.]*\d+\.?\d+[^\.]*$')
RE_DECIMAL = re.compile(r'[^\d.]+')
def setup(hass, config):
"""Set up the InfluxDB component."""
@@ -149,8 +152,6 @@ def setup(hass, config):
}
]
non_digit_tail = re.compile(r'[\d.]+')
non_decimal = re.compile(r'[^\d.]+')
for key, value in state.attributes.items():
if key != 'unit_of_measurement':
# If the key is already in fields
@@ -164,10 +165,12 @@ def setup(hass, config):
json_body[0]['fields'][key] = float(value)
except (ValueError, TypeError):
new_key = "{}_str".format(key)
json_body[0]['fields'][new_key] = str(value)
if non_digit_tail.match(json_body[0]['fields'][new_key]):
new_value = str(value)
json_body[0]['fields'][new_key] = new_value
if RE_DIGIT_TAIL.match(new_value):
json_body[0]['fields'][key] = float(
non_decimal.sub('', value))
RE_DECIMAL.sub('', new_value))
json_body[0]['tags'].update(tags)
-55
View File
@@ -1,55 +0,0 @@
"""
Support for Insteon Hub.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_hub/
"""
import logging
import voluptuous as vol
from homeassistant.const import (CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['insteon_hub==0.4.5']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'insteon_hub'
INSTEON = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Insteon Hub component.
This will automatically import associated lights.
"""
_LOGGER.warning("Component disabled at request from Insteon. "
"For more information: https://goo.gl/zLJaic")
return False
# pylint: disable=unreachable
import insteon
username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
api_key = config[DOMAIN][CONF_API_KEY]
global INSTEON
INSTEON = insteon.Insteon(username, password, api_key)
if INSTEON is None:
_LOGGER.error("Could not connect to Insteon service")
return False
discovery.load_platform(hass, 'light', DOMAIN, {}, config)
return True
+118 -20
View File
@@ -51,7 +51,7 @@ def setup(hass, config):
res = KNXTUNNEL.connect()
_LOGGER.debug("Res = %s", res)
if not res:
_LOGGER.exception("Could not connect to KNX/IP interface %s", host)
_LOGGER.error("Could not connect to KNX/IP interface %s", host)
return False
except KNXException as ex:
@@ -127,7 +127,10 @@ class KNXGroupAddress(Entity):
self._config = config
self._state = False
self._data = None
_LOGGER.debug("Initalizing KNX group address %s", self.address)
_LOGGER.debug(
"Initalizing KNX group address for %s (%s)",
self.name, self.address
)
def handle_knx_message(addr, data):
"""Handle an incoming KNX frame.
@@ -198,11 +201,15 @@ class KNXGroupAddress(Entity):
self._data = res
else:
_LOGGER.debug(
"Unable to read from KNX address: %s (None)", self.address)
"%s: unable to read from KNX address: %s (None)",
self.name, self.address
)
except KNXException:
_LOGGER.exception(
"Unable to read from KNX address: %s", self.address)
"%s: unable to read from KNX address: %s",
self.name, self.address
)
return False
@@ -213,9 +220,6 @@ class KNXMultiAddressDevice(Entity):
to be controlled by multiple group addresses.
"""
names = {}
values = {}
def __init__(self, hass, config, required, optional=None):
"""Initialize the device.
@@ -226,33 +230,69 @@ class KNXMultiAddressDevice(Entity):
"""
from knxip.core import parse_group_address, KNXException
self.names = {}
self.values = {}
self._config = config
self._state = False
self._data = None
_LOGGER.debug("Initalizing KNX multi address device")
_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:
_LOGGER.info(name)
paramname = '{}{}'.format(name, '_address')
addr = self._config.config.get(paramname)
addr = settings.get(paramname)
if addr is None:
_LOGGER.exception(
"Required KNX group address %s missing", paramname)
_LOGGER.error(
"%s: Required KNX group address %s missing",
self.name, paramname
)
raise KNXException(
"Group address for %s missing in configuration", paramname)
"%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 = self._config.config.get(paramname)
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("Cannot parse group address %s", addr)
_LOGGER.exception(
"%s: cannot parse group address %s",
self.name, addr
)
self.names[addr] = name
@property
@@ -280,11 +320,53 @@ class KNXMultiAddressDevice(Entity):
This is mostly important for optional addresses.
"""
for attributename, dummy_attribute in self.names.items():
for attributename in self.names.values():
if attributename == name:
return True
return False
def set_percentage(self, name, percentage):
"""Set a percentage in knx for a given attribute.
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 get_percentage(self, name):
"""Get a percentage from knx for a given attribute.
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))
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
return summed_value
def value(self, name):
"""Return the value to a given named attribute."""
from knxip.core import KNXException
@@ -295,13 +377,21 @@ class KNXMultiAddressDevice(Entity):
addr = attributeaddress
if addr is None:
_LOGGER.exception("Attribute %s undefined", name)
_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("Unable to read from KNX address: %s", addr)
_LOGGER.exception(
"%s: unable to read from KNX address: %s",
self.name, addr
)
return False
return res
@@ -316,13 +406,21 @@ class KNXMultiAddressDevice(Entity):
addr = attributeaddress
if addr is None:
_LOGGER.exception("Attribute %s undefined", name)
_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("Unable to write to KNX address: %s", addr)
_LOGGER.exception(
"%s: unable to write to KNX address: %s",
self.name, addr
)
return False
return True
+4 -4
View File
@@ -14,7 +14,7 @@ from homeassistant.components.light import (
PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['decora==0.4']
REQUIREMENTS = ['decora==0.6']
_LOGGER = logging.getLogger(__name__)
@@ -59,7 +59,7 @@ class DecoraLight(Light):
self._switch = decora.decora(self._address, self._key)
self._switch.connect()
self._state = self._switch.get_on()
self._brightness = self._switch.get_brightness()
self._brightness = self._switch.get_brightness() * 2.55
self.is_valid = True
@property
@@ -99,7 +99,7 @@ class DecoraLight(Light):
def set_state(self, brightness):
"""Set the state of this lamp to the provided brightness."""
self._switch.set_brightness(brightness)
self._switch.set_brightness(int(brightness / 2.55))
self._brightness = brightness
return True
@@ -120,5 +120,5 @@ class DecoraLight(Light):
def update(self):
"""Synchronise internal state with the actual light state."""
self._brightness = self._switch.get_brightness()
self._brightness = self._switch.get_brightness() * 2.55
self._state = self._switch.get_on()
@@ -1,79 +0,0 @@
"""
Support for Insteon Hub lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/insteon_hub/
"""
from homeassistant.components.insteon_hub import INSTEON
from homeassistant.components.light import (ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS, Light)
DEPENDENCIES = ['insteon_hub']
SUPPORT_INSTEON_HUB = SUPPORT_BRIGHTNESS
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon Hub light platform."""
devs = []
for device in INSTEON.devices:
if device.DeviceCategory == "Switched Lighting Control":
devs.append(InsteonToggleDevice(device))
if device.DeviceCategory == "Dimmable Lighting Control":
devs.append(InsteonToggleDevice(device))
add_devices(devs)
class InsteonToggleDevice(Light):
"""An abstract Class for an Insteon node."""
def __init__(self, node):
"""Initialize the device."""
self.node = node
self._value = 0
@property
def name(self):
"""Return the the name of the node."""
return self.node.DeviceName
@property
def unique_id(self):
"""Return the ID of this insteon node."""
return self.node.DeviceID
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._value / 100 * 255
def update(self):
"""Update state of the sensor."""
resp = self.node.send_command('get_status', wait=True)
try:
self._value = resp['response']['level']
except KeyError:
pass
@property
def is_on(self):
"""Return the boolean response if the node is on."""
return self._value != 0
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_INSTEON_HUB
def turn_on(self, **kwargs):
"""Turn device on."""
if ATTR_BRIGHTNESS in kwargs:
self._value = kwargs[ATTR_BRIGHTNESS] / 255 * 100
self.node.send_command('on', self._value)
else:
self._value = 100
self.node.send_command('on')
def turn_off(self, **kwargs):
"""Turn device off."""
self.node.send_command('off')
+1 -1
View File
@@ -56,7 +56,7 @@ class ISYLightDevice(isy.ISYDevice, Light):
def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device."""
if not self._node.off():
_LOGGER.debug("Unable to turn on light")
_LOGGER.debug("Unable to turn off light")
def turn_on(self, brightness=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device."""
@@ -11,18 +11,19 @@ import math
from os import path
from functools import partial
from datetime import timedelta
import async_timeout
import voluptuous as vol
from homeassistant.components.light import (
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_TRANSITION, ATTR_EFFECT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT,
preprocess_turn_on_alternatives)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant import util
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_point_in_utc_time
@@ -30,34 +31,79 @@ from homeassistant.helpers.service import extract_entity_ids
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
from . import effects as lifx_effects
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['aiolifx==0.4.8']
REQUIREMENTS = ['aiolifx==0.5.0', 'aiolifx_effects==0.1.0']
UDP_BROADCAST_PORT = 56700
# Delay (in ms) expected for changes to take effect in the physical bulb
BULB_LATENCY = 500
CONF_SERVER = 'server'
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
ATTR_HSBK = 'hsbk'
ATTR_INFRARED = 'infrared'
ATTR_POWER = 'power'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
})
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
ATTR_INFRARED = 'infrared'
ATTR_POWER = 'power'
LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({
ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
ATTR_POWER: cv.boolean,
})
SERVICE_EFFECT_PULSE = 'lifx_effect_pulse'
SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop'
SERVICE_EFFECT_STOP = 'lifx_effect_stop'
ATTR_POWER_ON = 'power_on'
ATTR_MODE = 'mode'
ATTR_PERIOD = 'period'
ATTR_CYCLES = 'cycles'
ATTR_SPREAD = 'spread'
ATTR_CHANGE = 'change'
PULSE_MODE_BLINK = 'blink'
PULSE_MODE_BREATHE = 'breathe'
PULSE_MODE_PING = 'ping'
PULSE_MODE_STROBE = 'strobe'
PULSE_MODE_SOLID = 'solid'
PULSE_MODES = [PULSE_MODE_BLINK, PULSE_MODE_BREATHE, PULSE_MODE_PING,
PULSE_MODE_STROBE, PULSE_MODE_SOLID]
LIFX_EFFECT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=True): cv.boolean,
})
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)),
ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)),
ATTR_MODE: vol.In(PULSE_MODES),
})
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)),
})
LIFX_EFFECT_STOP_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@@ -71,27 +117,79 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
server_addr = config.get(CONF_SERVER)
lifx_manager = LIFXManager(hass, async_add_devices)
lifx_discovery = aiolifx.LifxDiscovery(hass.loop, lifx_manager)
coro = hass.loop.create_datagram_endpoint(
partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager),
local_addr=(server_addr, UDP_BROADCAST_PORT))
lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT))
hass.async_add_job(coro)
lifx_effects.setup(hass, lifx_manager)
@callback
def cleanup(event):
"""Clean up resources."""
lifx_discovery.cleanup()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
return True
def find_hsbk(**kwargs):
"""Find the desired color from a number of possible inputs."""
hue, saturation, brightness, kelvin = [None]*4
preprocess_turn_on_alternatives(kwargs)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
saturation = convert_8_to_16(saturation)
brightness = convert_8_to_16(brightness)
kelvin = 3500
if ATTR_XY_COLOR in kwargs:
hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
saturation = convert_8_to_16(saturation)
kelvin = 3500
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
saturation = 0
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
hsbk = [hue, saturation, brightness, kelvin]
return None if hsbk == [None]*4 else hsbk
def merge_hsbk(base, change):
"""Copy change on top of base, except when None."""
if change is None:
return None
return list(map(lambda x, y: y if y is not None else x, base, change))
class LIFXManager(object):
"""Representation of all known LIFX entities."""
def __init__(self, hass, async_add_devices):
"""Initialize the light."""
import aiolifx_effects
self.entities = {}
self.hass = hass
self.async_add_devices = async_add_devices
self.effects_conductor = aiolifx_effects.Conductor(loop=hass.loop)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
self.register_set_state(descriptions)
self.register_effects(descriptions)
def register_set_state(self, descriptions):
"""Register the LIFX set_state service call."""
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service."""
@@ -99,22 +197,73 @@ class LIFXManager(object):
for light in self.service_to_entities(service):
if service.service == SERVICE_LIFX_SET_STATE:
task = light.async_set_state(**service.data)
tasks.append(hass.async_add_job(task))
tasks.append(self.hass.async_add_job(task))
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=self.hass.loop)
descriptions = self.get_descriptions()
hass.services.async_register(
self.hass.services.async_register(
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle,
descriptions.get(SERVICE_LIFX_SET_STATE),
schema=LIFX_SET_STATE_SCHEMA)
@staticmethod
def get_descriptions():
"""Load and return descriptions for our own service calls."""
return load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
def register_effects(self, descriptions):
"""Register the LIFX effects as hass service calls."""
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service, i.e. start an effect."""
entities = self.service_to_entities(service)
if entities:
yield from self.start_effect(
entities, service.service, **service.data)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
descriptions.get(SERVICE_EFFECT_PULSE),
schema=LIFX_EFFECT_PULSE_SCHEMA)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_COLORLOOP),
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_STOP),
schema=LIFX_EFFECT_STOP_SCHEMA)
@asyncio.coroutine
def start_effect(self, entities, service, **kwargs):
"""Start a light effect on entities."""
import aiolifx_effects
devices = list(map(lambda l: l.device, entities))
if service == SERVICE_EFFECT_PULSE:
effect = aiolifx_effects.EffectPulse(
power_on=kwargs.get(ATTR_POWER_ON),
period=kwargs.get(ATTR_PERIOD),
cycles=kwargs.get(ATTR_CYCLES),
mode=kwargs.get(ATTR_MODE),
hsbk=find_hsbk(**kwargs),
)
yield from self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_COLORLOOP:
preprocess_turn_on_alternatives(kwargs)
brightness = None
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
effect = aiolifx_effects.EffectColorloop(
power_on=kwargs.get(ATTR_POWER_ON),
period=kwargs.get(ATTR_PERIOD),
change=kwargs.get(ATTR_CHANGE),
spread=kwargs.get(ATTR_SPREAD),
transition=kwargs.get(ATTR_TRANSITION),
brightness=brightness,
)
yield from self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_STOP:
yield from self.effects_conductor.stop(devices)
def service_to_entities(self, service):
"""Return the known devices that a service call mentions."""
@@ -148,7 +297,7 @@ class LIFXManager(object):
@callback
def ready(self, device, msg):
"""Handle the device once all data is retrieved."""
entity = LIFXLight(device)
entity = LIFXLight(device, self.effects_conductor)
_LOGGER.debug("%s register READY", entity.who)
self.entities[device.mac_addr] = entity
self.async_add_devices([entity])
@@ -182,17 +331,13 @@ class AwaitAioLIFX:
@asyncio.coroutine
def wait(self, method):
"""Call an aiolifx method and wait for its response or a timeout."""
"""Call an aiolifx method and wait for its response."""
self.device = None
self.message = None
self.event.clear()
method(self.callback)
while self.light.available and not self.event.is_set():
try:
with async_timeout.timeout(1.0, loop=self.light.hass.loop):
yield from self.event.wait()
except asyncio.TimeoutError:
pass
method(callb=self.callback)
yield from self.event.wait()
return self.message
@@ -209,17 +354,13 @@ def convert_16_to_8(value):
class LIFXLight(Light):
"""Representation of a LIFX light."""
def __init__(self, device):
def __init__(self, device, effects_conductor):
"""Initialize the light."""
self.device = device
self.effects_conductor = effects_conductor
self.registered = True
self.product = device.product
self.blocker = None
self.effect_data = None
self.postponed_update = None
self._name = device.label
self.set_power(device.power_level)
self.set_color(*device.color)
@property
def lifxwhite(self):
@@ -235,34 +376,33 @@ class LIFXLight(Light):
@property
def name(self):
"""Return the name of the device."""
return self._name
return self.device.label
@property
def who(self):
"""Return a string identifying the device."""
ip_addr = '-'
if self.device:
ip_addr = self.device.ip_addr[0]
return "%s (%s)" % (ip_addr, self.name)
return "%s (%s)" % (self.device.ip_addr, self.name)
@property
def rgb_color(self):
"""Return the RGB value."""
_LOGGER.debug(
"rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2])
return self._rgb
hue, sat, bri, _ = self.device.color
return color_util.color_hsv_to_RGB(
hue, convert_16_to_8(sat), convert_16_to_8(bri))
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
brightness = convert_16_to_8(self._bri)
brightness = convert_16_to_8(self.device.color[2])
_LOGGER.debug("brightness: %d", brightness)
return brightness
@property
def color_temp(self):
"""Return the color temperature."""
temperature = color_util.color_temperature_kelvin_to_mired(self._kel)
kelvin = self.device.color[3]
temperature = color_util.color_temperature_kelvin_to_mired(kelvin)
_LOGGER.debug("color_temp: %d", temperature)
return temperature
@@ -290,13 +430,15 @@ class LIFXLight(Light):
@property
def is_on(self):
"""Return true if device is on."""
_LOGGER.debug("is_on: %d", self._power)
return self._power != 0
return self.device.power_level != 0
@property
def effect(self):
"""Return the currently running effect."""
return self.effect_data.effect.name if self.effect_data else None
"""Return the name of the currently running effect."""
effect = self.effects_conductor.effect(self.device)
if effect:
return 'lifx_effect_' + effect.name
return None
@property
def supported_features(self):
@@ -311,38 +453,35 @@ class LIFXLight(Light):
@property
def effect_list(self):
"""Return the list of supported effects."""
return lifx_effects.effect_list(self)
"""Return the list of supported effects for this light."""
if self.lifxwhite:
return [
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
@asyncio.coroutine
def update_after_transition(self, now):
"""Request new status after completion of the last transition."""
self.postponed_update = None
yield from self.refresh_state()
yield from self.async_update_ha_state()
@asyncio.coroutine
def unblock_updates(self, now):
"""Allow async_update after the new state has settled on the bulb."""
self.blocker = None
yield from self.refresh_state()
yield from self.async_update()
yield from self.async_update_ha_state()
def update_later(self, when):
"""Block immediate update requests and schedule one for later."""
if self.blocker:
self.blocker()
self.blocker = async_track_point_in_utc_time(
self.hass, self.unblock_updates,
util.dt.utcnow() + timedelta(milliseconds=BULB_LATENCY))
"""Schedule an update requests when a transition is over."""
if self.postponed_update:
self.postponed_update()
self.postponed_update = None
if when > BULB_LATENCY:
if when > 0:
self.postponed_update = async_track_point_in_utc_time(
self.hass, self.update_after_transition,
util.dt.utcnow() + timedelta(milliseconds=when+BULB_LATENCY))
util.dt.utcnow() + timedelta(milliseconds=when))
@asyncio.coroutine
def async_turn_on(self, **kwargs):
@@ -359,10 +498,10 @@ class LIFXLight(Light):
@asyncio.coroutine
def async_set_state(self, **kwargs):
"""Set a color on the light and turn it on/off."""
yield from self.stop_effect()
yield from self.effects_conductor.stop([self.device])
if ATTR_EFFECT in kwargs:
yield from lifx_effects.default_effect(self, **kwargs)
yield from self.default_effect(**kwargs)
return
if ATTR_INFRARED in kwargs:
@@ -377,124 +516,44 @@ class LIFXLight(Light):
power_on = kwargs.get(ATTR_POWER, False)
power_off = not kwargs.get(ATTR_POWER, True)
hsbk, changed_color = self.find_hsbk(**kwargs)
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
self.who, self._power, fade, *hsbk)
hsbk = merge_hsbk(self.device.color, find_hsbk(**kwargs))
if self._power == 0:
# Send messages, waiting for ACK each time
ack = AwaitAioLIFX(self).wait
bulb = self.device
if not self.is_on:
if power_off:
self.device.set_power(False, None, 0)
if changed_color:
self.device.set_color(hsbk, None, 0)
yield from ack(partial(bulb.set_power, False))
if hsbk:
yield from ack(partial(bulb.set_color, hsbk))
if power_on:
self.device.set_power(True, None, fade)
yield from ack(partial(bulb.set_power, True, duration=fade))
else:
if power_on:
self.device.set_power(True, None, 0)
if changed_color:
self.device.set_color(hsbk, None, fade)
yield from ack(partial(bulb.set_power, True))
if hsbk:
yield from ack(partial(bulb.set_color, hsbk, duration=fade))
if power_off:
self.device.set_power(False, None, fade)
yield from ack(partial(bulb.set_power, False, duration=fade))
if power_on:
self.update_later(0)
else:
self.update_later(fade)
# Schedule an update when the transition is complete
self.update_later(fade)
if fade <= BULB_LATENCY:
if power_on:
self.set_power(1)
if power_off:
self.set_power(0)
if changed_color:
self.set_color(*hsbk)
@asyncio.coroutine
def default_effect(self, **kwargs):
"""Start an effect with default parameters."""
service = kwargs[ATTR_EFFECT]
data = {
ATTR_ENTITY_ID: self.entity_id,
}
yield from self.hass.services.async_call(DOMAIN, service, data)
@asyncio.coroutine
def async_update(self):
"""Update bulb status (if it is available)."""
"""Update bulb status."""
_LOGGER.debug("%s async_update", self.who)
if self.blocker is None:
yield from self.refresh_state()
@asyncio.coroutine
def stop_effect(self):
"""Stop the currently running effect (if any)."""
if self.effect_data:
yield from self.effect_data.effect.async_restore(self)
@asyncio.coroutine
def refresh_state(self):
"""Ask the device about its current state and update our copy."""
if self.available:
msg = yield from AwaitAioLIFX(self).wait(self.device.get_color)
if msg is not None:
self.set_power(self.device.power_level)
self.set_color(*self.device.color)
self._name = self.device.label
def find_hsbk(self, **kwargs):
"""Find the desired color from a number of possible inputs."""
changed_color = False
hsbk = kwargs.pop(ATTR_HSBK, None)
if hsbk is not None:
return [hsbk, True]
preprocess_turn_on_alternatives(kwargs)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
saturation = convert_8_to_16(saturation)
brightness = convert_8_to_16(brightness)
changed_color = True
else:
hue = self._hue
saturation = self._sat
brightness = self._bri
if ATTR_XY_COLOR in kwargs:
hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
saturation = convert_8_to_16(saturation)
changed_color = True
# When color or temperature is set, use a default value for the other
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
if not changed_color:
saturation = 0
changed_color = True
else:
if changed_color:
kelvin = 3500
else:
kelvin = self._kel
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
changed_color = True
else:
brightness = self._bri
return [[hue, saturation, brightness, kelvin], changed_color]
def set_power(self, power):
"""Set power state value."""
_LOGGER.debug("set_power: %d", power)
self._power = (power != 0)
def set_color(self, hue, sat, bri, kel):
"""Set color state values."""
self._hue = hue
self._sat = sat
self._bri = bri
self._kel = kel
red, green, blue = color_util.color_hsv_to_RGB(
hue, convert_16_to_8(sat), convert_16_to_8(bri))
_LOGGER.debug("set_color: %d %d %d %d [%d %d %d]",
hue, sat, bri, kel, red, green, blue)
self._rgb = [red, green, blue]
# Avoid state ping-pong by holding off updates as the state settles
yield from asyncio.sleep(0.25)
yield from AwaitAioLIFX(self).wait(self.device.get_color)
@@ -1,388 +0,0 @@
"""Support for light effects for the LIFX light platform."""
import logging
import asyncio
import random
import voluptuous as vol
from homeassistant.components.light import (
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_EFFECT, ATTR_TRANSITION,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
from homeassistant.const import (ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
SERVICE_EFFECT_BREATHE = 'lifx_effect_breathe'
SERVICE_EFFECT_PULSE = 'lifx_effect_pulse'
SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop'
SERVICE_EFFECT_STOP = 'lifx_effect_stop'
ATTR_POWER_ON = 'power_on'
ATTR_PERIOD = 'period'
ATTR_CYCLES = 'cycles'
ATTR_MODE = 'mode'
ATTR_SPREAD = 'spread'
ATTR_CHANGE = 'change'
MODE_BLINK = 'blink'
MODE_BREATHE = 'breathe'
MODE_PING = 'ping'
MODE_STROBE = 'strobe'
MODE_SOLID = 'solid'
MODES = [MODE_BLINK, MODE_BREATHE, MODE_PING, MODE_STROBE, MODE_SOLID]
# aiolifx waveform modes
WAVEFORM_SINE = 1
WAVEFORM_PULSE = 4
NEUTRAL_WHITE = 3500
LIFX_EFFECT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=True): cv.boolean,
})
LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)),
ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)),
})
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA.extend({
vol.Optional(ATTR_MODE, default=MODE_BLINK): vol.In(MODES),
})
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
vol.Optional(ATTR_PERIOD, default=60):
vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
vol.Optional(ATTR_CHANGE, default=20):
vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
vol.Optional(ATTR_SPREAD, default=30):
vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)),
})
LIFX_EFFECT_STOP_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=False): cv.boolean,
})
def setup(hass, lifx_manager):
"""Register the LIFX effects as hass service calls."""
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service."""
entities = lifx_manager.service_to_entities(service)
if entities:
yield from start_effect(hass, entities,
service.service, **service.data)
descriptions = lifx_manager.get_descriptions()
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_BREATHE, async_service_handle,
descriptions.get(SERVICE_EFFECT_BREATHE),
schema=LIFX_EFFECT_BREATHE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
descriptions.get(SERVICE_EFFECT_PULSE),
schema=LIFX_EFFECT_PULSE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_COLORLOOP),
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_STOP),
schema=LIFX_EFFECT_STOP_SCHEMA)
@asyncio.coroutine
def start_effect(hass, devices, service, **data):
"""Start a light effect."""
tasks = []
for light in devices:
tasks.append(hass.async_add_job(light.stop_effect()))
yield from asyncio.wait(tasks, loop=hass.loop)
if service in SERVICE_EFFECT_BREATHE:
effect = LIFXEffectBreathe(hass, devices)
elif service in SERVICE_EFFECT_PULSE:
effect = LIFXEffectPulse(hass, devices)
elif service == SERVICE_EFFECT_COLORLOOP:
effect = LIFXEffectColorloop(hass, devices)
elif service == SERVICE_EFFECT_STOP:
effect = LIFXEffectStop(hass, devices)
hass.async_add_job(effect.async_perform(**data))
@asyncio.coroutine
def default_effect(light, **kwargs):
"""Start an effect with default parameters."""
service = kwargs[ATTR_EFFECT]
data = {
ATTR_ENTITY_ID: light.entity_id,
}
yield from light.hass.services.async_call(DOMAIN, service, data)
def effect_list(light):
"""Return the list of supported effects for this light."""
if light.lifxwhite:
return [
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
else:
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
class LIFXEffectData(object):
"""Structure describing a running effect."""
def __init__(self, effect, power, color):
"""Initialize data structure."""
self.effect = effect
self.power = power
self.color = color
class LIFXEffect(object):
"""Representation of a light effect running on a number of lights."""
def __init__(self, hass, lights):
"""Initialize the effect."""
self.hass = hass
self.lights = lights
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Do common setup and play the effect."""
yield from self.async_setup(**kwargs)
yield from self.async_play(**kwargs)
@asyncio.coroutine
def async_setup(self, **kwargs):
"""Prepare all lights for the effect."""
for light in self.lights:
# Remember the current state (as far as we know it)
yield from light.refresh_state()
light.effect_data = LIFXEffectData(
self, light.is_on, light.device.color)
# Temporarily turn on power for the effect to be visible
if kwargs[ATTR_POWER_ON] and not light.is_on:
hsbk = self.from_poweroff_hsbk(light, **kwargs)
light.device.set_color(hsbk)
light.device.set_power(True)
# pylint: disable=no-self-use
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect."""
yield None
@asyncio.coroutine
def async_restore(self, light):
"""Restore to the original state (if we are still running)."""
if light in self.lights:
self.lights.remove(light)
if light.effect_data and light.effect_data.effect == self:
if not light.effect_data.power:
light.device.set_power(False)
yield from asyncio.sleep(0.5)
light.device.set_color(light.effect_data.color)
yield from asyncio.sleep(0.5)
light.effect_data = None
yield from light.refresh_state()
def from_poweroff_hsbk(self, light, **kwargs):
"""Return the color when starting from a powered off state."""
return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE]
class LIFXEffectPulse(LIFXEffect):
"""Representation of a pulse effect."""
def __init__(self, hass, lights):
"""Initialize the pulse effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_PULSE
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect on all lights."""
for light in self.lights:
self.hass.async_add_job(self.async_light_play(light, **kwargs))
@asyncio.coroutine
def async_light_play(self, light, **kwargs):
"""Play a light effect on the bulb."""
hsbk, color_changed = light.find_hsbk(**kwargs)
if kwargs[ATTR_MODE] == MODE_STROBE:
# Strobe must flash from a dark color
light.device.set_color([0, 0, 0, NEUTRAL_WHITE])
yield from asyncio.sleep(0.1)
default_period = 0.1
default_cycles = 10
else:
default_period = 1.0
default_cycles = 1
period = kwargs.get(ATTR_PERIOD, default_period)
cycles = kwargs.get(ATTR_CYCLES, default_cycles)
# Breathe has a special waveform
if kwargs[ATTR_MODE] == MODE_BREATHE:
waveform = WAVEFORM_SINE
else:
waveform = WAVEFORM_PULSE
# Ping and solid have special duty cycles
if kwargs[ATTR_MODE] == MODE_PING:
ping_duration = int(5000 - min(2500, 300*period))
duty_cycle = 2**15 - ping_duration
elif kwargs[ATTR_MODE] == MODE_SOLID:
duty_cycle = -2**15
else:
duty_cycle = 0
# Set default effect color based on current setting
if not color_changed:
if kwargs[ATTR_MODE] == MODE_STROBE:
# Strobe: cold white
hsbk = [hsbk[0], 0, 65535, 5600]
elif light.lifxwhite or hsbk[1] < 65536/2:
# White: toggle brightness
hsbk[2] = 65535 if hsbk[2] < 65536/2 else 0
else:
# Color: fully desaturate with full brightness
hsbk = [hsbk[0], 0, 65535, 4000]
# Start the effect
args = {
'transient': 1,
'color': hsbk,
'period': int(period*1000),
'cycles': cycles,
'duty_cycle': duty_cycle,
'waveform': waveform,
}
light.device.set_waveform(args)
# Wait for completion and restore the initial state
yield from asyncio.sleep(period*cycles)
yield from self.async_restore(light)
def from_poweroff_hsbk(self, light, **kwargs):
"""Return the color is the target color, but no brightness."""
hsbk, _ = light.find_hsbk(**kwargs)
return [hsbk[0], hsbk[1], 0, hsbk[2]]
class LIFXEffectBreathe(LIFXEffectPulse):
"""Representation of a breathe effect."""
def __init__(self, hass, lights):
"""Initialize the breathe effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_BREATHE
_LOGGER.warning("'lifx_effect_breathe' is deprecated. Please use "
"'lifx_effect_pulse' with 'mode: breathe'")
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Prepare all lights for the effect."""
kwargs[ATTR_MODE] = MODE_BREATHE
yield from super().async_perform(**kwargs)
class LIFXEffectColorloop(LIFXEffect):
"""Representation of a colorloop effect."""
def __init__(self, hass, lights):
"""Initialize the colorloop effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_COLORLOOP
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect on all lights."""
period = kwargs[ATTR_PERIOD]
spread = kwargs[ATTR_SPREAD]
change = kwargs[ATTR_CHANGE]
direction = 1 if random.randint(0, 1) else -1
# Random start
hue = random.uniform(0, 360) % 360
while self.lights:
hue = (hue + direction*change) % 360
random.shuffle(self.lights)
lhue = hue
for light in self.lights:
if ATTR_TRANSITION in kwargs:
transition = int(1000*kwargs[ATTR_TRANSITION])
elif light == self.lights[0] or spread > 0:
transition = int(1000 * random.uniform(period/2, period))
if ATTR_BRIGHTNESS in kwargs:
brightness = int(65535/255*kwargs[ATTR_BRIGHTNESS])
else:
brightness = light.effect_data.color[2]
hsbk = [
int(65535/360*lhue),
int(random.uniform(0.8, 1.0)*65535),
brightness,
NEUTRAL_WHITE,
]
light.device.set_color(hsbk, None, transition)
# Adjust the next light so the full spread is used
if len(self.lights) > 1:
lhue = (lhue + spread/(len(self.lights)-1)) % 360
yield from asyncio.sleep(period)
class LIFXEffectStop(LIFXEffect):
"""A no-op effect, but starting it will stop an existing effect."""
def __init__(self, hass, lights):
"""Initialize the stop effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_STOP
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Do nothing."""
yield None
@@ -1,98 +0,0 @@
lifx_set_state:
description: Set a color/brightness and possibliy turn the light on/off
fields:
entity_id:
description: Name(s) of entities to set a state on
example: 'light.garage'
'...':
description: All turn_on parameters can be used to specify a color
infrared:
description: Automatic infrared level (0..255) when light brightness is low
example: 255
transition:
description: Duration in seconds it takes to get to the final state
example: 10
power:
description: Turn the light on (True) or off (False). Leave out to keep the power as it is.
example: True
lifx_effect_breathe:
description: Deprecated, use lifx_effect_pulse
lifx_effect_pulse:
description: Run a flash effect by changing to a color and back.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.kitchen'
mode:
description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid'
example: strobe
brightness:
description: Number between 0..255 indicating brightness of the temporary color
example: 120
color_name:
description: A human readable color name
example: 'red'
rgb_color:
description: The temporary color in RGB-format
example: '[255, 100, 100]'
period:
description: Duration of the effect in seconds (default 1.0)
example: 3
cycles:
description: Number of times the effect should run (default 1.0)
example: 2
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_colorloop:
description: Run an effect with looping colors.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.disco1, light.disco2, light.disco3'
brightness:
description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light
example: 120
period:
description: Duration (in seconds) between color changes (default 60)
example: 180
change:
description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20)
example: 45
spread:
description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30)
example: 0
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_stop:
description: Stop a running effect.
fields:
entity_id:
description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere.
example: 'light.bedroom'
+21 -13
View File
@@ -24,11 +24,13 @@ CONF_BRIDGES = 'bridges'
CONF_GROUPS = 'groups'
CONF_NUMBER = 'number'
CONF_VERSION = 'version'
CONF_FADE = 'fade'
DEFAULT_LED_TYPE = 'rgbw'
DEFAULT_PORT = 5987
DEFAULT_TRANSITION = 0
DEFAULT_VERSION = 6
DEFAULT_FADE = False
LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led']
@@ -58,6 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TYPE, default=DEFAULT_LED_TYPE):
vol.In(LED_TYPE),
vol.Required(CONF_NUMBER): cv.positive_int,
vol.Optional(CONF_FADE, default=DEFAULT_FADE): cv.boolean,
}
]),
},
@@ -112,7 +115,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
group_conf.get(CONF_NUMBER),
group_conf.get(CONF_NAME),
group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE))
lights.append(LimitlessLEDGroup.factory(group))
lights.append(LimitlessLEDGroup.factory(group, {
'fade': group_conf[CONF_FADE]
}))
add_devices(lights)
@@ -152,25 +157,26 @@ def state(new_state):
class LimitlessLEDGroup(Light):
"""Representation of a LimitessLED group."""
def __init__(self, group):
def __init__(self, group, config):
"""Initialize a group."""
self.group = group
self.repeating = False
self._is_on = False
self._brightness = None
self.config = config
@staticmethod
def factory(group):
def factory(group, config):
"""Produce LimitlessLEDGroup objects."""
from limitlessled.group.rgbw import RgbwGroup
from limitlessled.group.white import WhiteGroup
from limitlessled.group.rgbww import RgbwwGroup
if isinstance(group, WhiteGroup):
return LimitlessLEDWhiteGroup(group)
return LimitlessLEDWhiteGroup(group, config)
elif isinstance(group, RgbwGroup):
return LimitlessLEDRGBWGroup(group)
return LimitlessLEDRGBWGroup(group, config)
elif isinstance(group, RgbwwGroup):
return LimitlessLEDRGBWWGroup(group)
return LimitlessLEDRGBWWGroup(group, config)
@property
def should_poll(self):
@@ -196,15 +202,17 @@ class LimitlessLEDGroup(Light):
def turn_off(self, transition_time, pipeline, **kwargs):
"""Turn off a group."""
if self.is_on:
pipeline.transition(transition_time, brightness=0.0).off()
if self.config[CONF_FADE]:
pipeline.transition(transition_time, brightness=0.0)
pipeline.off()
class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED White group."""
def __init__(self, group):
def __init__(self, group, config):
"""Initialize White group."""
super().__init__(group)
super().__init__(group, config)
# Initialize group with known values.
self.group.on = True
self.group.temperature = 1.0
@@ -242,9 +250,9 @@ class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED RGBW group."""
def __init__(self, group):
def __init__(self, group, config):
"""Initialize RGBW group."""
super().__init__(group)
super().__init__(group, config)
# Initialize group with known values.
self.group.on = True
self.group.white()
@@ -301,9 +309,9 @@ class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
class LimitlessLEDRGBWWGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED RGBWW group."""
def __init__(self, group):
def __init__(self, group, config):
"""Initialize RGBWW group."""
super().__init__(group)
super().__init__(group, config)
# Initialize group with known values.
self.group.on = True
self.group.white()
@@ -101,3 +101,98 @@ hue_activate_scene:
scene_name:
description: Name of hue scene from the hue app
example: "Energize"
lifx_set_state:
description: Set a color/brightness and possibliy turn the light on/off
fields:
entity_id:
description: Name(s) of entities to set a state on
example: 'light.garage'
'...':
description: All turn_on parameters can be used to specify a color
infrared:
description: Automatic infrared level (0..255) when light brightness is low
example: 255
transition:
description: Duration in seconds it takes to get to the final state
example: 10
power:
description: Turn the light on (True) or off (False). Leave out to keep the power as it is.
example: True
lifx_effect_pulse:
description: Run a flash effect by changing to a color and back.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.kitchen'
mode:
description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid'
example: strobe
brightness:
description: Number between 0..255 indicating brightness of the temporary color
example: 120
color_name:
description: A human readable color name
example: 'red'
rgb_color:
description: The temporary color in RGB-format
example: '[255, 100, 100]'
period:
description: Duration of the effect in seconds (default 1.0)
example: 3
cycles:
description: Number of times the effect should run (default 1.0)
example: 2
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_colorloop:
description: Run an effect with looping colors.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.disco1, light.disco2, light.disco3'
brightness:
description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light
example: 120
period:
description: Duration (in seconds) between color changes (default 60)
example: 180
change:
description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20)
example: 45
spread:
description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30)
example: 0
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_stop:
description: Stop a running effect.
fields:
entity_id:
description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere.
example: 'light.bedroom'
+5 -2
View File
@@ -79,5 +79,8 @@ class VeraLight(VeraDevice, Light):
def update(self):
"""Call to update state."""
self._state = self.vera_device.is_switched_on()
self._brightness = self.vera_device.get_brightness()
self._color = self.vera_device.get_color()
if self.vera_device.is_dimmable:
# If it is dimmable, both functions exist. In case color
# is not supported, it will return None
self._brightness = self.vera_device.get_brightness()
self._color = self.vera_device.get_color()
+53 -26
View File
@@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/verisure/
"""
import logging
from time import sleep
from time import time
from homeassistant.components.verisure import HUB as hub
from homeassistant.components.verisure import (CONF_LOCKS, CONF_CODE_DIGITS)
from homeassistant.components.lock import LockDevice
@@ -19,28 +20,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Verisure platform."""
locks = []
if int(hub.config.get(CONF_LOCKS, 1)):
hub.update_locks()
hub.update_overview()
locks.extend([
VerisureDoorlock(device_id)
for device_id in hub.lock_status
])
VerisureDoorlock(device_label)
for device_label in hub.get(
"$.doorLockStatusList[*].deviceLabel")])
add_devices(locks)
class VerisureDoorlock(LockDevice):
"""Representation of a Verisure doorlock."""
def __init__(self, device_id):
def __init__(self, device_label):
"""Initialize the Verisure lock."""
self._id = device_id
self._device_label = device_label
self._state = STATE_UNKNOWN
self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None
self._change_timestamp = 0
@property
def name(self):
"""Return the name of the lock."""
return '{}'.format(hub.lock_status[self._id].location)
return hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].area",
self._device_label)
@property
def state(self):
@@ -50,7 +55,9 @@ class VerisureDoorlock(LockDevice):
@property
def available(self):
"""Return True if entity is available."""
return hub.available
return hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')]",
self._device_label) is not None
@property
def changed_by(self):
@@ -64,32 +71,52 @@ class VerisureDoorlock(LockDevice):
def update(self):
"""Update lock status."""
hub.update_locks()
if hub.lock_status[self._id].status == 'unlocked':
if time() - self._change_timestamp < 10:
return
hub.update_overview()
status = hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState",
self._device_label)
if status == 'UNLOCKED':
self._state = STATE_UNLOCKED
elif hub.lock_status[self._id].status == 'locked':
elif status == 'LOCKED':
self._state = STATE_LOCKED
elif hub.lock_status[self._id].status != 'pending':
_LOGGER.error(
"Unknown lock state %s", hub.lock_status[self._id].status)
self._changed_by = hub.lock_status[self._id].name
elif status != 'PENDING':
_LOGGER.error('Unknown lock state %s', status)
self._changed_by = hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].userString",
self._device_label)
@property
def is_locked(self):
"""Return true if lock is locked."""
return hub.lock_status[self._id].status
return self._state == STATE_LOCKED
def unlock(self, **kwargs):
"""Send unlock command."""
hub.my_pages.lock.set(kwargs[ATTR_CODE], self._id, 'UNLOCKED')
_LOGGER.debug("Verisure doorlock unlocking")
hub.my_pages.lock.wait_while_pending()
self.update()
if self._state == STATE_UNLOCKED:
return
self.set_lock_state(kwargs[ATTR_CODE], STATE_UNLOCKED)
def lock(self, **kwargs):
"""Send lock command."""
hub.my_pages.lock.set(kwargs[ATTR_CODE], self._id, 'LOCKED')
_LOGGER.debug("Verisure doorlock locking")
hub.my_pages.lock.wait_while_pending()
self.update()
if self._state == STATE_LOCKED:
return
self.set_lock_state(kwargs[ATTR_CODE], STATE_LOCKED)
def set_lock_state(self, code, state):
"""Send set lock state command."""
lock_state = 'lock' if state == STATE_LOCKED else 'unlock'
transaction_id = hub.session.set_lock_state(
code,
self._device_label,
lock_state)['doorLockStateChangeTransactionId']
_LOGGER.debug("Verisure doorlock %s", state)
transaction = {}
while 'result' not in transaction:
sleep(0.5)
transaction = hub.session.get_lock_state_transaction(
transaction_id)
if transaction['result'] == 'OK':
self._state = state
self._change_timestamp = time()
@@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player.denon/
"""
import logging
from collections import namedtuple
import voluptuous as vol
from homeassistant.components.media_player import (
@@ -16,16 +17,19 @@ 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_NAME, STATE_ON, CONF_ZONE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.4.4']
REQUIREMENTS = ['denonavr==0.5.1']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = None
DEFAULT_SHOW_SOURCES = False
CONF_SHOW_ALL_SOURCES = 'show_all_sources'
CONF_ZONES = 'zones'
CONF_VALID_ZONES = ['Zone2', 'Zone3']
CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)'
KEY_DENON_CACHE = 'denonavr_hosts'
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
@@ -36,16 +40,26 @@ SUPPORT_MEDIA_MODES = SUPPORT_PLAY_MEDIA | \
SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET | SUPPORT_PLAY
DENON_ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE): vol.In(CONF_VALID_ZONES, CONF_INVALID_ZONES_ERR),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES):
cv.boolean,
vol.Optional(CONF_ZONES):
vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA])
})
NewHost = namedtuple('NewHost', ['host', 'name'])
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Denon platform."""
# pylint: disable=import-error
import denonavr
# Initialize list with receivers to be started
@@ -55,28 +69,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if cache is None:
cache = hass.data[KEY_DENON_CACHE] = set()
# Start assignment of host and name
# Get config option for show_all_sources
show_all_sources = config.get(CONF_SHOW_ALL_SOURCES)
# Get config option for additional zones
zones = config.get(CONF_ZONES)
if zones is not None:
add_zones = {}
for entry in zones:
add_zones[entry[CONF_ZONE]] = entry[CONF_NAME]
else:
add_zones = None
# Start assignment of host and name
new_hosts = []
# 1. option: manual setting
if config.get(CONF_HOST) is not None:
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
# Check if host not in cache, append it and save for later starting
if host not in cache:
cache.add(host)
receivers.append(
DenonDevice(denonavr.DenonAVR(host, name, show_all_sources)))
_LOGGER.info("Denon receiver at host %s initialized", host)
new_hosts.append(NewHost(host=host, name=name))
# 2. option: discovery using netdisco
if discovery_info is not None:
host = discovery_info.get('host')
name = discovery_info.get('name')
# Check if host not in cache, append it and save for later starting
if host not in cache:
cache.add(host)
receivers.append(
DenonDevice(denonavr.DenonAVR(host, name, show_all_sources)))
_LOGGER.info("Denon receiver at host %s initialized", host)
new_hosts.append(NewHost(host=host, name=name))
# 3. option: discovery using denonavr library
if config.get(CONF_HOST) is None and discovery_info is None:
d_receivers = denonavr.discover()
@@ -85,14 +103,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for d_receiver in d_receivers:
host = d_receiver["host"]
name = d_receiver["friendlyName"]
# Check if host not in cache, append it and save for later
# starting
if host not in cache:
cache.add(host)
receivers.append(
DenonDevice(
denonavr.DenonAVR(host, name, show_all_sources)))
_LOGGER.info("Denon receiver at host %s initialized", host)
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)
for new_zone in new_device.zones.values():
receivers.append(DenonDevice(new_zone))
cache.add(host)
_LOGGER.info("Denon receiver at host %s initialized", host)
# Add all freshly discovered receivers
if receivers:
@@ -21,7 +21,7 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyemby==1.2']
REQUIREMENTS = ['pyemby==1.3']
_LOGGER = logging.getLogger(__name__)
+15 -8
View File
@@ -540,7 +540,7 @@ class KodiDevice(MediaPlayerDevice):
elif self._turn_off_action == 'shutdown':
yield from self.server.System.Shutdown()
else:
_LOGGER.warning('turn_off requested but turn_off_action is none')
_LOGGER.warning("turn_off requested but turn_off_action is none")
@cmd
@asyncio.coroutine
@@ -694,22 +694,26 @@ class KodiDevice(MediaPlayerDevice):
def async_call_method(self, method, **kwargs):
"""Run Kodi JSONRPC API method with params."""
import jsonrpc_base
_LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs)
_LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs)
result_ok = False
try:
result = yield from getattr(self.server, method)(**kwargs)
result_ok = True
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error']
_LOGGER.error('Run API method %s.%s(%s) error: %s',
_LOGGER.error("Run API method %s.%s(%s) error: %s",
self.entity_id, method, kwargs, result)
except jsonrpc_base.jsonrpc.TransportError:
result = None
_LOGGER.warning("TransportError trying to run API method "
"%s.%s(%s)", self.entity_id, method, kwargs)
if isinstance(result, dict):
event_data = {'entity_id': self.entity_id,
'result': result,
'result_ok': result_ok,
'input': {'method': method, 'params': kwargs}}
_LOGGER.debug('EVENT kodi_call_method_result: %s', event_data)
_LOGGER.debug("EVENT kodi_call_method_result: %s", event_data)
self.hass.bus.async_fire(EVENT_KODI_CALL_METHOD_RESULT,
event_data=event_data)
return result
@@ -753,10 +757,13 @@ class KodiDevice(MediaPlayerDevice):
yield from self.server.Playlist.Add(params)
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error']
_LOGGER.error('Run API method %s.Playlist.Add(%s) error: %s',
_LOGGER.error("Run API method %s.Playlist.Add(%s) error: %s",
self.entity_id, media_type, result)
except jsonrpc_base.jsonrpc.TransportError:
_LOGGER.warning("TransportError trying to add playlist to %s",
self.entity_id)
else:
_LOGGER.warning('No media detected for Playlist.Add')
_LOGGER.warning("No media detected for Playlist.Add")
@asyncio.coroutine
def async_add_all_albums(self, artist_name):
@@ -800,7 +807,7 @@ class KodiDevice(MediaPlayerDevice):
artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid']
except KeyError:
_LOGGER.warning('No artists were found: %s', artist_name)
_LOGGER.warning("No artists were found: %s", artist_name)
return None
@asyncio.coroutine
@@ -839,7 +846,7 @@ class KodiDevice(MediaPlayerDevice):
album_name, [a['label'] for a in albums['albums']])
return albums['albums'][out[0][0]]['albumid']
except KeyError:
_LOGGER.warning('No albums were found with artist: %s, album: %s',
_LOGGER.warning("No albums were found with artist: %s, album: %s",
artist_name, album_name)
return None
+21 -2
View File
@@ -14,7 +14,8 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, MEDIA_TYPE_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_SHUFFLE_SET,
SUPPORT_SEEK, MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD,
CONF_HOST, CONF_NAME)
@@ -32,7 +33,8 @@ PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=120)
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE
SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE | \
SUPPORT_CLEAR_PLAYLIST | SUPPORT_SHUFFLE_SET | SUPPORT_SEEK
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
@@ -266,3 +268,20 @@ class MpdDevice(MediaPlayerDevice):
self.client.clear()
self.client.add(media_id)
self.client.play()
@property
def shuffle(self):
"""Boolean if shuffle is enabled."""
return bool(self.status['random'])
def set_shuffle(self, shuffle):
"""Enable/disable shuffle mode."""
self.client.random(int(shuffle))
def clear_playlist(self):
"""Clear players playlist."""
self.client.clear()
def media_seek(self, position):
"""Send seek command."""
self.client.seekcur(position)
@@ -88,6 +88,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
elif discovery_info is not None:
# Parse discovery data
host = discovery_info.get('host')
port = discovery_info.get('port')
host = '%s:%s' % (host, port)
_LOGGER.info("Discovered PLEX server: %s", host)
if host in _CONFIGURING:
@@ -106,6 +108,7 @@ def setup_plexserver(host, token, hass, config, add_devices_callback):
try:
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
_LOGGER.info("Discovery configuration done (no token needed)")
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
plexapi.exceptions.NotFound) as error:
_LOGGER.info(error)
@@ -20,7 +20,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
STATE_PAUSED, STATE_PLAYING,
STATE_UNAVAILABLE)
REQUIREMENTS = ['libsoundtouch==0.3.0']
REQUIREMENTS = ['libsoundtouch==0.6.2']
_LOGGER = logging.getLogger(__name__)
+1 -1
View File
@@ -29,7 +29,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA
REQUIREMENTS = ['paho-mqtt==1.2.3']
REQUIREMENTS = ['paho-mqtt==1.3.0']
_LOGGER = logging.getLogger(__name__)
+12 -8
View File
@@ -17,8 +17,8 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC
_LOGGER = logging.getLogger(__name__)
TOPIC_MATCHER = re.compile(
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/(?P<object_id>[a-zA-Z0-9_-]+)'
'/config')
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/'
r'(?:(?P<node_id>[a-zA-Z0-9_-]+)/)?(?P<object_id>[a-zA-Z0-9_-]+)/config')
SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor', 'switch']
@@ -44,7 +44,7 @@ def async_start(hass, discovery_topic, hass_config):
if not match:
return
prefix_topic, component, object_id = match.groups()
prefix_topic, component, node_id, object_id = match.groups()
try:
payload = json.loads(payload)
@@ -65,21 +65,25 @@ def async_start(hass, discovery_topic, hass_config):
payload[CONF_PLATFORM] = platform
if CONF_STATE_TOPIC not in payload:
payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format(
discovery_topic, component, object_id)
payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format(
discovery_topic, component, '%s/' % node_id if node_id else '',
object_id)
if ALREADY_DISCOVERED not in hass.data:
hass.data[ALREADY_DISCOVERED] = set()
discovery_hash = (component, object_id)
# If present, the node_id will be included in the discovered object id
discovery_id = '_'.join((node_id, object_id)) if node_id else object_id
discovery_hash = (component, discovery_id)
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
_LOGGER.info("Component has already been discovered: %s %s",
component, object_id)
component, discovery_id)
return
hass.data[ALREADY_DISCOVERED].add(discovery_hash)
_LOGGER.info("Found new component: %s %s", component, object_id)
_LOGGER.info("Found new component: %s %s", component, discovery_id)
yield from async_load_platform(
hass, component, platform, payload, hass_config)
@@ -0,0 +1,80 @@
"""
Clicksend platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.clicksend/
"""
import json
import logging
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_API_KEY, CONF_RECIPIENT, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_JSON)
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
BASE_API_URL = 'https://rest.clicksend.com/v3'
HEADERS = {HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_RECIPIENT): cv.string,
})
def get_service(hass, config, discovery_info=None):
"""Get the ClickSend notification service."""
if _authenticate(config) is False:
_LOGGER.exception("You are not authorized to access ClickSend")
return None
return ClicksendNotificationService(config)
class ClicksendNotificationService(BaseNotificationService):
"""Implementation of a notification service for the ClickSend service."""
def __init__(self, config):
"""Initialize the service."""
self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY)
self.recipient = config.get(CONF_RECIPIENT)
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
data = ({'messages': [{'source': 'hass.notify', 'from': self.recipient,
'to': self.recipient, 'body': message}]})
api_url = "{}/sms/send".format(BASE_API_URL)
resp = requests.post(api_url, data=json.dumps(data), headers=HEADERS,
auth=(self.username, self.api_key), timeout=5)
obj = json.loads(resp.text)
response_msg = obj['response_msg']
response_code = obj['response_code']
if resp.status_code != 200:
_LOGGER.error("Error %s : %s (Code %s)", resp.status_code,
response_msg, response_code)
def _authenticate(config):
"""Authenticate with ClickSend."""
api_url = '{}/account'.format(BASE_API_URL)
resp = requests.get(api_url, headers=HEADERS,
auth=(config.get(CONF_USERNAME),
config.get(CONF_API_KEY)), timeout=5)
if resp.status_code != 200:
return False
return True
+1 -1
View File
@@ -25,7 +25,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components.frontend import add_manifest_json_key
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['pywebpush==1.0.4', 'PyJWT==1.5.0']
REQUIREMENTS = ['pywebpush==1.0.5', 'PyJWT==1.5.0']
DEPENDENCIES = ['frontend']
+17 -10
View File
@@ -29,16 +29,18 @@ _LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments
ATTR_HTML = 'html'
CONF_STARTTLS = 'starttls'
CONF_ENCRYPTION = 'encryption'
CONF_DEBUG = 'debug'
CONF_SERVER = 'server'
CONF_SENDER_NAME = 'sender_name'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 25
DEFAULT_PORT = 587
DEFAULT_TIMEOUT = 5
DEFAULT_DEBUG = False
DEFAULT_STARTTLS = False
DEFAULT_ENCRYPTION = 'starttls'
ENCRYPTION_OPTIONS = ['tls', 'starttls', 'none']
# pylint: disable=no-value-for-parameter
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -47,7 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
vol.Optional(CONF_ENCRYPTION, default=DEFAULT_ENCRYPTION):
vol.In(ENCRYPTION_OPTIONS),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SENDER_NAME): cv.string,
@@ -62,7 +65,7 @@ def get_service(hass, config, discovery_info=None):
config.get(CONF_PORT),
config.get(CONF_TIMEOUT),
config.get(CONF_SENDER),
config.get(CONF_STARTTLS),
config.get(CONF_ENCRYPTION),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
config.get(CONF_RECIPIENT),
@@ -78,28 +81,32 @@ def get_service(hass, config, discovery_info=None):
class MailNotificationService(BaseNotificationService):
"""Implement the notification service for E-mail messages."""
def __init__(self, server, port, timeout, sender, starttls, username,
def __init__(self, server, port, timeout, sender, encryption, username,
password, recipients, sender_name, debug):
"""Initialize the SMTP service."""
self._server = server
self._port = port
self._timeout = timeout
self._sender = sender
self.starttls = starttls
self.encryption = encryption
self.username = username
self.password = password
self.recipients = recipients
self._sender_name = sender_name
self._timeout = timeout
self.debug = debug
self.tries = 2
def connect(self):
"""Connect/authenticate to SMTP Server."""
mail = smtplib.SMTP(self._server, self._port, timeout=self._timeout)
if self.encryption == "tls":
mail = smtplib.SMTP_SSL(
self._server, self._port, timeout=self._timeout)
else:
mail = smtplib.SMTP(
self._server, self._port, timeout=self._timeout)
mail.set_debuglevel(self.debug)
mail.ehlo_or_helo_if_needed()
if self.starttls:
if self.encryption == "starttls":
mail.starttls()
mail.ehlo()
if self.username and self.password:
+2 -1
View File
@@ -9,7 +9,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import sanitize_filename
DOMAIN = 'python_script'
REQUIREMENTS = ['restrictedpython==4.0a2']
REQUIREMENTS = ['restrictedpython==4.0a3']
FOLDER = 'python_scripts'
_LOGGER = logging.getLogger(__name__)
@@ -92,6 +92,7 @@ def execute(hass, filename, source, data=None):
'_print_': StubPrinter,
'_getattr_': protected_getattr,
'_write_': full_write_guard,
'_getiter_': iter,
}
logger = logging.getLogger('{}.{}'.format(__name__, filename))
local = {
@@ -33,7 +33,7 @@ from . import purge, migration
from .const import DATA_INSTANCE
from .util import session_scope
REQUIREMENTS = ['sqlalchemy==1.1.10']
REQUIREMENTS = ['sqlalchemy==1.1.11']
_LOGGER = logging.getLogger(__name__)
@@ -64,6 +64,8 @@ def _apply_update(engine, new_version):
# Create indexes for states
_create_index(engine, "states", "ix_states_last_updated")
_create_index(engine, "states", "ix_states_entity_id_created")
elif new_version == 3:
_create_index(engine, "states", "ix_states_created_domain")
else:
raise ValueError("No schema migration defined for version {}"
.format(new_version))
+4 -2
View File
@@ -16,7 +16,7 @@ from homeassistant.remote import JSONEncoder
# pylint: disable=invalid-name
Base = declarative_base()
SCHEMA_VERSION = 2
SCHEMA_VERSION = 3
_LOGGER = logging.getLogger(__name__)
@@ -75,7 +75,9 @@ class States(Base): # type: ignore
Index('states__significant_changes',
'domain', 'last_updated', 'entity_id'),
Index('ix_states_entity_id_created',
'entity_id', 'created'),)
'entity_id', 'created'),
Index('ix_states_created_domain',
'created', 'domain'),)
@staticmethod
def from_event(event):
+52 -15
View File
@@ -26,12 +26,13 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 5222
DEVICES = []
CONF_DEVICE_CACHE = 'device_cache'
SERVICE_SYNC = 'harmony_sync'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(ATTR_ACTIVITY, default=None): cv.string,
})
@@ -44,29 +45,65 @@ HARMONY_SYNC_SCHEMA = vol.Schema({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Harmony platform."""
import pyharmony
global DEVICES
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
_LOGGER.debug("Loading Harmony platform: %s", name)
host = None
activity = None
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
if CONF_DEVICE_CACHE not in hass.data:
hass.data[CONF_DEVICE_CACHE] = []
if discovery_info:
# Find the discovered device in the list of user configurations
override = next((c for c in hass.data[CONF_DEVICE_CACHE]
if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)),
False)
port = DEFAULT_PORT
if override:
activity = override.get(ATTR_ACTIVITY)
port = override.get(CONF_PORT, DEFAULT_PORT)
host = (
discovery_info.get(CONF_NAME),
discovery_info.get(CONF_HOST),
port)
# Ignore hub name when checking if this hub is known - ip and port only
if host and host[1:] in set([h[1:] for h in DEVICES]):
_LOGGER.debug("Discovered host already known: %s", host)
return
elif CONF_HOST in config:
host = (
config.get(CONF_NAME),
config.get(CONF_HOST),
config.get(CONF_PORT),
)
activity = config.get(ATTR_ACTIVITY)
else:
hass.data[CONF_DEVICE_CACHE].append(config)
return
name, address, port = host
_LOGGER.info("Loading Harmony Platform: %s at %s:%s, startup activity: %s",
name, address, port, activity)
try:
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
host, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
address, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(address, port))
_LOGGER.debug("Received token: %s", token)
except ValueError as err:
_LOGGER.warning("%s for remote: %s", err.args[0], name)
return False
_LOGGER.debug("Received token: %s", token)
DEVICES = [HarmonyRemote(
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
add_devices(DEVICES, True)
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
device = HarmonyRemote(
name, address, port,
activity, harmony_conf_file, token)
DEVICES.append(device)
add_devices([device])
register_services(hass)
return True

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