Compare commits

..

189 Commits

Author SHA1 Message Date
Paulus Schoutsen
6902e522b9 Merge pull request #7335 from home-assistant/release-0-43-2
0.43.2
2017-04-27 12:42:25 -07:00
Paulus Schoutsen
6750e33525 Right fix for Python Open Z-Wave in Docker (#7337) 2017-04-27 09:28:40 -07:00
Paulus Schoutsen
e6a690558b Fix breaking SSL in test HTML5 (#7310) 2017-04-27 09:27:31 -07:00
Adam Mills
a3bc559fa3 Version bump for automatic (#7329) 2017-04-27 09:05:58 -07:00
Adam Mills
3a1072a158 Version bump of aioautomatic (#7300)
* Version bump of aioautomatic

* Update requirements_all.txt
2017-04-27 09:05:58 -07:00
Paulus Schoutsen
0841bf8529 Update frontend (#7324)
* Initial version of hassio panel

* Update frontend
2017-04-27 09:03:54 -07:00
Fabian Affolter
2b82c222b0 Upgrade python-telegram-bot to 5.3.1 (#7311) 2017-04-27 09:03:46 -07:00
Paulus Schoutsen
d966129fd8 Upgrade pytradfri to 1.1 (#7290) 2017-04-27 09:03:25 -07:00
Paulus Schoutsen
84752b3b13 Hassio api v3 (#7323)
* HassIO rest API v3

* fix content type

* fix lint

* Update comment

* fix content type

* change proxy handling

* fix handling

* fix register

* fix addons

* fix routing

* Update hassio to just proxy

* Fix tests

* Lint
2017-04-27 09:02:43 -07:00
Pascal Vizeli
3735c2e761 Fix HassIO bug with supervisor update & log (#7282) 2017-04-27 09:02:43 -07:00
Pascal Vizeli
9fc89ba744 WIP: HassIO allow to access to container logs. (#7271)
* HassIO allow to access to container logs.

* Add unittest & make a fixture for env

* Add unittest to check if no env exists

* Fix lint
2017-04-27 09:02:43 -07:00
Paulus Schoutsen
f7603a421f Version bump to 0.43.2 2017-04-27 09:02:08 -07:00
Paulus Schoutsen
758aed07e8 Merge pull request #7288 from home-assistant/release-0-43-1
0.43.1
2017-04-24 21:02:39 -07:00
Paulus Schoutsen
166bcc0687 Recorder: Check for ENTITY_ID key that contains None value (#7287) 2017-04-24 20:51:14 -07:00
Klaas Hoekema
41a803a7be Work around bad content-type in Hook api response (#7267) 2017-04-24 20:36:16 -07:00
Martin Hjelmare
1b55dbeb44 Fix telegram webhooks (#7236)
* Always register the view if a webhook exists.
* Return True if platform is set up succesfully, False otherwise.
* Remove the webhook when home assistant stops. Webhooks and long
  polling are mutually excklusive. If a webhook is left after home
  assistant is stopped, a polling telegram bot is unable to be set up,
  on next start of home assistant.
2017-04-24 20:36:08 -07:00
Greg Dowling
6c594e20f6 Workround for wemo subscription bug. (#7245) 2017-04-24 20:35:59 -07:00
Fabian Affolter
c8404cb299 Upgrade paho-mqtt to 1.2.3 (#7214) 2017-04-24 20:35:51 -07:00
John Arild Berentsen
79c6467797 Zwave cover workaround for graber shades. (#7204)
* wierd pylint complaint

* Workaround for Graber csz1 shades

* logging

* Try direct

* Try direct

* Use workaround

* Review changes and tests

* test

* reset test

* Use Bright and Dim also as open and close is
2017-04-24 20:35:27 -07:00
Paulus Schoutsen
e01c36c906 Version bump to 0.43.1 2017-04-24 20:34:56 -07:00
Paulus Schoutsen
8be2ac70ec Merge pull request #7197 from home-assistant/release-0-43
0.43
2017-04-22 00:33:01 -07:00
Paulus Schoutsen
477ebd99b4 Version bump to 0.43 2017-04-22 00:32:27 -07:00
Martin Hjelmare
093c7f0e44 Fix tradfri lights (#7212)
* Remove leftover use of slugify

* The IKEA manufacturer key is now exactly as found in device info.

* Fix bitwise addition of supported features
2017-04-22 00:31:44 -07:00
Paulus Schoutsen
80e9e9bfda tradfri: Improve color temp support detection (#7211) 2017-04-21 23:33:25 -07:00
Henrik Nicolaisen
dafbdbd2d0 Issue 6749 updated pylgtv to 0.1.6 to fix thread leak in asyncio loop (#7199)
* updated pylgtv module to fix problems with timeouts

* - update pylgtv to 0.1.6
- handle new TimeoutError exception from pylgtv

* used full name for exception handling of concurrent.futures._base.TimeoutError

* the exception handling should now follow the rules

* float typecasting should not be necessary

* use asyncio for TimeoutError it’s an alias for concurrent.futures.TimeoutError
2017-04-21 20:24:33 -07:00
Sean Dague
b641f6863c Fix arwn platform to update hass state when events are received (#7202)
The arwn platform was refactored to be asyncio friendly, however in
doing so one thing was missed which was explicitly telling hass when
something interesting has happened. This led to the very interesting
to debug issue that the state cards were all out of date, even though
the graphs were not.
2017-04-21 20:22:49 -07:00
Nikolas Beutler
07fcf22aeb Update ios.py (#7160)
* Update ios.py

as discussed. the part: 
       if battery_state == ios.ATTR_BATTERY_STATE_FULL:
            returning_icon_level = DEFAULT_ICON_LEVEL
kinda screws up the charging icon.

i might just miss a logical solution for that though.
let me know what you think. it might not be beautiful but i think its an overall improve over the current "double battery" solution

* Update ios.py

chound fix and full_battery_charge fix

* Update ios.py

removed new line

* Update ios.py

* Update ios.py

* Update ios.py

* Update ios.py

* Update ios.py

* Update ios.py

* merged request from robbie

* Update ios.py

* Update ios.py

* Update ios.py
2017-04-21 20:18:31 -07:00
Anders Melchiorsen
2d57c6a1c7 Support xy_color with LIFX lights (#7208) 2017-04-21 20:18:31 -07:00
Pascal Vizeli
5c737cfa6e HassIO API v2 (#7201)
Add an optional extended description…
2017-04-21 12:21:55 +02:00
Anders Melchiorsen
e3f682c7d3 LIFX light effects (#7145)
* Refactor into find_hsbk

This will be useful for new methods that also have to find passed in colors.

* Add AwaitAioLIFX

This encapsulates the callback and Event that aiolifx needs and thus avoids an
explosion of those when new calls are added.

The refresh_state is now generally useful, so move it into its own method.

* Initial effects support for LIFX

These effects are useful as notifications. They mimic the breathe and pulse
effects from the LIFX HTTP API:

    https://api.developer.lifx.com/docs/breathe-effect
    https://api.developer.lifx.com/docs/pulse-effect

However, this implementation runs locally with the LIFX LAN protocol.

* Saturate LIFX no color value

Now the color is "full saturation, no brightness". This avoids a lot of
temporary white when fading from the "no color" value and into a real color.

* Organize LIFX effects in classes

This is to move the setup/restore away from the actual effect, making it quite
simple to add additional effects.

* Stop running LIFX effects on conflicting service calls

Turning the light on/off or starting a new effect will now stop the running
effect.

* Present default LIFX effects as light.turn_on effects

This makes the effects (with default parameters) easily accessible from
the UI.

* Add LIFX colorloop effect

This cycles the HSV colors, so that is added as an internal way to set a
color.

* Move lifx to its own package and split effects into a separate file

* Always show LIFX light name in logs

The name is actually the easiest way to identify a bulb so just using it
as a fallback was a bit odd.

* Compact effect getter

* Always use full brightness for random flash color

This is a stopgap. When a bit more infrastructure is in place, the intention
is to turn the current hue some degrees. This will guarantee a flash color
that is both unlike the current color and unlike white.

* Clear effects concurrently

We have to wait for the bulbs, so let us wait for all of them at once.

* Add lifx_effect_stop

The colorloop effect is most impressive if run on many lights. Testing
this has revealed the need for an easy way to stop effects on all lights
and return to the initial state of each bulb. This new call does just that.

Calling turn_on/turn_off could also stop the effect but that would not
restore the initial state.

* Always calculate the initial effect color

To fade nicely from power off, the breathe effect needs to keep an
unchanging hue. So give up on using a static start color and just find the
correct hue from the target color.

The colorloop effect can start from anything but we use a random color
just to keep things a little interesting during power on.

* Fix lint

* Update .coveragerc
2017-04-20 22:46:29 -07:00
Paulus Schoutsen
dbb0525311 Merge branch 'master' into dev 2017-04-20 22:39:09 -07:00
Pascal Vizeli
f641287aa2 Add HassIO to discovery component (#7195)
* Add HassIO to autodiscovery

* Fix tests

* fix tests

* fix test v2

* fix mock test

* call
2017-04-20 18:45:15 -07:00
LvivEchoes
bbeb64eb24 Add support of input registers while querying modbus sensor. (#7082)
* Add support of input registers while querying modbus sensor.

* Changed config option. Refactoring.
2017-04-20 21:28:49 -04:00
Fabian Affolter
eb2e5e5b9d Upgrade py-cpuinfo to 3.2.0 (#7190) 2017-04-20 12:07:56 +02:00
mountainsandcode
920d298c7e mvglive bug fixes and improvements (#6953)
* Refactored mvglive.py

This pull requests builds on the first work with the mvglive sensor:
- Refactoring the code so that multiple sensors for departures can be added
- Rewrites the transport mode restrictions ("products") to be more modular
- Fixes bugs, such as missing implementation of line restriction
- Other improvements, such as including data attribution

* Further improvements to mvglive sensor

- The API returns the property 'direction', which can be used to filter U-Bahn trains by direction without having to enter all final destinations
- The sensor icon now corresponds to the mode of transport of the next departure

* UBahnDirection refactored

U-Bahn SEV (bus replacement services) have unexpected direction values, fixed resulting bug and hound issues
2017-04-20 00:11:55 -07:00
Pierre Ståhl
93820d5124 Do not request artwork if not available (#7189)
This should fix the "dancing media player" issue where the media player
requests artwork when it's not really available, making the UI "dance".
2017-04-20 00:10:06 -07:00
Pierre Ståhl
2e11d49af3 Fix auto discovery for Apple TV (#7188) 2017-04-20 00:09:27 -07:00
Mitko Masarliev
2d5ab520ef Fix for errors on missing preview on LG webos TV (#6755)
* fix for missing image preview on LG webos TV

* fix to use largeIcon if it start with http
2017-04-20 00:08:53 -07:00
Sören Oldag
0c14c66fbc Added light.pwm component. (#7009)
* Added light.pwm component.

* Renamed pwm platform to rpi_gpio_pwm.

* Update requirements_all.txt
2017-04-19 23:32:20 -07:00
Fabian Affolter
b1621d4175 Add ping binary sensor (#7052)
* Add ping binary sensor

* Fix typo and lint issues

* Use SCAN_INTERVAL
2017-04-19 23:15:26 -07:00
happyleavesaoc
1860b6c521 opensky sensor (#7061)
* opensky sensor

* address opensky review comments

* update opensky distance calc
2017-04-19 22:56:20 -07:00
Gianluca Barbaro
f59b3da5fe JSON MQTT Device tracker (#7055)
* ready for PR

* minor fix

* another minor fix

* new platform mqtt_json

* using ATTR constants

* voluptuous check on JSON payload

* voluptuous check on JSON payload
2017-04-19 22:53:07 -07:00
happyleavesaoc
e020d5114a spotify media player (#6980)
* spotify media player

* fix refresh token

* spotify improvements

* add checks for idle

* import STATE_IDLE

* support more media_ids, limit updates

* move spotify device update

* Remove schedule_update_ha_state because should_poll is true
2017-04-19 22:45:12 -07:00
Paulus Schoutsen
ce51866bd2 Update frontend 2017-04-19 22:03:48 -07:00
Charles Blonde
931fce8239 Add Bose soundtouch discovery support and upgrade libsoundtouch library (#7005)
* Add Bose soundtouch discovery support and upgrade libsoundtouch library

* Remove DEVICE global variable

* Update netdisco to lastest version
2017-04-19 21:52:37 -07:00
Paulus Schoutsen
76d2154820 Fix wemo discovery (#7183)
* Fix wemo discovery

* Fix key
2017-04-19 21:25:45 -07:00
Henrik Nicolaisen
b985e4ef0b updated pylgtv module to fix problems with timeouts (#7184) 2017-04-19 19:36:11 -04:00
Gianluca Barbaro
632256fae2 Mqtt camera test (#7175)
* mock mqtt

* minor fix

* minor fix

* minor fix
2017-04-19 09:26:44 -07:00
Paulus Schoutsen
9b43b39370 Update frontend 2017-04-19 09:24:02 -07:00
Paulus Schoutsen
1a635fede3 Tweak Tradfri (#7172) 2017-04-19 09:15:39 -07:00
Paulus Schoutsen
90baa2ce4d Add history to component priority list (#7173) 2017-04-19 09:15:18 -07:00
Sytone
2f4b2ddc0a Add condition for API failure (#7181)
* Add condition for API failure

If you are not running the latest ve3rsion of ZM this will cause exceptions to fire. This fix handles a response from ZM but a non successful attempt. 

This resolves the issue https://github.com/home-assistant/home-assistant/issues/7178

* Fixed houndci-bot issues
2017-04-19 09:08:48 -07:00
Fabian Affolter
921760f8c1 Upgrade mypy to 0.501 (was renamed from mypy-lang) (#7117) 2017-04-19 14:34:23 +02:00
Fabian Affolter
8ba41563c9 Disable invalid-sequence-index (#7177) 2017-04-19 14:09:00 +02:00
Fabian Affolter
a41d0aced7 Supress trackback and upgrade PyMata to 2.14 (#7176) 2017-04-19 12:48:15 +02:00
Alessandro Mogavero
5179832f6f Added new services to platform kodi (#6426)
* added new service

* fixed basic test in kodi platform

* Added new method async_get_albums

* Added new methods in module kodi

* added method find_song in kodi module

* method add_song_to_playlist made
async

* Added media type to method async_play_media

* added methods async_clear_playlist
and play_song

* methods play_song and find_song
made async

* added new service play_song

* Improved kodi._find
now it find for whole words only

* added possibility to specify artist in
kodi.async_find_artist

* added kodi.async_find_album

* added new optional input to play_song service

* In async_play_song added handling of no song found

* default artist value changed to ''

* async_add_song_to_playlist now can also
search for musinc

* added service add_song_to_playlist

* Added new service add_album_to_playlist

* added services to switch shuffle mode

* added service add_all_albums_to_playlist

* handled error in async_unset_shuffle
and async_set_shuffle

* Added abstract methods to media_player

* _server substituted with server property

* style made consistent with requirements

* Fixed issue with pylint

* Services moved to kodi platform

* service play_song removed

* removed service unset_shuffle

* all add services merged into one

* removed service get_artists

* added kodi_ to service names

* Fixed some style issues

* Removed changes in media_player __init__

* Implemented requested changes

* Fixed pylint problem
2017-04-18 23:19:27 -07:00
Paulus Schoutsen
ce9bb0e84c Upgrade netdisco (#7171) 2017-04-18 20:58:25 -07:00
Johan Bloemberg
e5feeec7a4 Value of 0 should not be considered unknown. (#7139)
* Value of 0 should not be considered `unknown`.

* Reflect disconnect state in entity states.

* Due to adding unknown state on disconnect, the amount of reconnects can sometimes be more. Test for at least 2 reconnect attempts.
2017-04-18 20:24:44 -07:00
Paulus Schoutsen
4becfb66e3 Upgrade pytradfri to 1.0 (#7163) 2017-04-18 20:11:18 -07:00
Mitesh Patel
e4bbbe20dd Fix id zone mismatch (#7165)
* Fixes issue with id mismatch when multiple devices are connected to the lutron bridge

* Updates labels

* removes no longer needed config values.

* removes no longer needed imports
2017-04-19 00:24:58 +02:00
Erik Eriksson
1e758ed030 Keep track of already added players (#7149) 2017-04-19 00:20:52 +02:00
Michaël Arnauts
d007269ecc Update neato.py (#7166)
Fix leftover copy/paste error in comment of neato.py
2017-04-18 22:03:06 +02:00
Greg Dowling
bbad15f853 Add subscription update for Wemo switches, fix bug in Insight switches, fix wemo motion bug, fix wemo discovery (#7135)
* Fix wemo discovery.

* Bump wemo version, add subscription_update for basic switch, fix bug with turning insight switches off.

* Fix missed callback change for wemo motion.

* Regress netdisco changes.
2017-04-18 09:11:08 -07:00
Thibault Cohen
de71fee0a0 Fix #7026 adding a new wol parameter (#7144) 2017-04-18 09:09:06 -07:00
Fabian Affolter
c7a11277ac myStrom WiFi bulbs (#7161)
* Add initial support for myStrom WiFi bulbs

* Upgrade python-mystrom to 0.3.8

* Add myStrom light

* Fix lint issue
2017-04-18 09:03:56 -07:00
Paulus Schoutsen
5574686d74 Disable MQTT camera test (#7164) 2017-04-18 08:55:51 -07:00
Martin Hjelmare
15d8f8b827 Add support for tradfri color temp (#7153)
* Add support for tradfri color temp

* Only return rgb_color if supported features indicate support.
* Return color_temp if supported features indicate support and dict
  with allowed color temperatures exist for manufacturer.
* Add manufacturer specific supported features, with default.
* Color temperature is set from a dict of allowed pairs per
  manufacturer, where kelvin is mapped to hex_color. When a user sets a
  color temperature, the closest matching temperature is selected and
  set.

* Clean up
2017-04-18 08:26:59 -07:00
Greg Dowling
1925748f61 Add vera power meter. (#7134)
* Add vera power meter.

* Use W for power.
2017-04-18 12:01:23 +02:00
Mike Megally
226066eafd exposed content_type in rest_command (#7101)
* exposed content_type in rest_command, which allows for manually specifying the content_type for more-strict api endpoints

* fixed up column length

Length was 86 chars, and it needed to be 79

* double import of HTTP_HEADER_CONTENT_TYPE

Removed the accidental double-import of HTTP_HEADER_CONTENT_TYPE

* moved rest_command-specific config value into component

* if no content_type, default to None

* unit test

* newline

* unused CONTENT_TYPE_TEXT_PLAIN

* removed the http-agnostic abstraction hass provided in favor of aiohttps hdrs constant
2017-04-18 11:50:43 +02:00
Kevin
43799b8fee small fix for random effect in order to use the whole rgb range. So 255 is not excluded anymore. (#7156) 2017-04-18 11:46:18 +02:00
Robbie Trencheny
9c0171ec5e Track device last identify time 2017-04-17 23:31:50 -07:00
Robbie Trencheny
b7141901f6 Change iOS sensor unique ID to use the device ID 2017-04-17 22:58:04 -07:00
Robbie Trencheny
919bb08d02 Fix iOS icon calculation to return mdi:battery for levels above 95 2017-04-17 22:51:06 -07:00
Pascal Vizeli
cec39077ba Fix HassIO timeout bug (#7155)
* Fix HassIO timeout bug

* fix lint

* Add long polling timeout to stop event
2017-04-18 00:25:50 +02:00
thecynic
9ed4ed2e47 lutron: fix typo that prevented callback registration (#7148) 2017-04-17 23:04:44 +02:00
Anders Melchiorsen
d4b05a6a85 Fix LIFX lights with disappearing names (#7119)
* Cache the name of LIFX lights

After #7031 the LIFX device will change during an unregister/register
transition. This has the user-visible effect of the new device missing
a friendly name until the next poll.

We now cache the name internally and it will then transfer to the new
device when it registers.

* Allow LIFX logging even without an available device

This will allow us to set the device to None when it unregisters.

* Calculate LIFX availability from the existence of a device

This has become possible because the device is no longer needed
to provide the name of the light when it is unavailable.

We just have to forget the device when it unregisters.
2017-04-16 17:40:22 -07:00
Anders Melchiorsen
103377bdb0 Add LIFX Cloud scene support (#7124)
This uses the LIFX HTTP API to list and activate the scenes that are
stored in the LIFX cloud by the native LIFX smartphone apps.
2017-04-16 16:40:12 -07:00
Anders Melchiorsen
5fa8037231 Always return True/False from is_state and is_state_attr (#7138)
* Always return True/False from is_state and is_state_attr

These functions are documented to always return True/False but the
short-circuit evaluation would return None if the entity_id did not exist.

* Reword into a single statement
2017-04-16 16:36:15 -07:00
Paulus Schoutsen
a2d268a061 Merge pull request #7142 from home-assistant/release-0-42-4
0.42.4
2017-04-16 16:02:10 -07:00
Adam Mills
37f959eb02 Add debug logging to automation initial state (#7068) 2017-04-16 15:45:40 -07:00
Robbie Trencheny
1ce2b6357a Replace rollershutter with cover in demo (#7140) 2017-04-16 15:27:03 -07:00
Adam Mills
527223b992 Fix for zwave RGB setting (#7137) 2017-04-16 15:07:11 -07:00
Adam Mills
409fd62a7c Fix for zwave RGB setting (#7137) 2017-04-16 15:06:59 -07:00
Robbie Trencheny
fadd33bcb2 Make version number optional and a string to fix identify issue introduced in iOS 1.0.1 (#7141) 2017-04-16 15:00:08 -07:00
Fabian Affolter
904b017552 Upgrade aiohttp to 2.0.7 (#7106) 2017-04-16 15:00:08 -07:00
Martin Hjelmare
1efa6eaf0f Fix mysensors callback (#7057)
* Fix mysensors callback

* All messages was not triggering proper updates. Fix by checking all
  child value types each update.

* Upgrade mysensors dep

* Fix pickle persistence when upgrading.
2017-04-16 15:00:08 -07:00
Paulus Schoutsen
9744ec584a Version bump to 0.42.4 2017-04-16 14:59:15 -07:00
Robbie Trencheny
58dfc1d1b1 Make version number optional and a string to fix identify issue introduced in iOS 1.0.1 (#7141) 2017-04-16 14:53:03 -07:00
Paulus Schoutsen
951af6c76d Make Tradfri discoverable (#7128)
* Make Tradfri discoverable

* Fix lint errors

* Fix bugs and clean up calls to light_control

* Add more color util tests

* Add coap client to dockerfile
2017-04-16 14:37:39 -07:00
Patrik
75242e67a7 IKEA Tradfri Gateway: added support for RGB (#7115)
* After rebase and all fixes

* Added color_rgb_to_hex to util.color

* Added test_color_rgb_to_hex

* Changed reference to color_rgb_to_hex

* Bumped to pytradfri 0.5, having support for retry

* Bumped to pytradfri 0.5, having support for retry

* Bumped to pytradfri 0.5, having support for retry

* Bumped to pytradfri 0.5, having support for retry

* Rolled back to 0.4

* Rolled back to 0.4
2017-04-16 11:35:52 -07:00
Paulus Schoutsen
a1208261a8 Load zwave panel (#7127)
* Load Z-Wave panel when component loads

* Update frontend

* Fix tests
2017-04-16 11:10:55 -07:00
Martin Hjelmare
3528705afd Bump version to 0.43.0.dev0 (#7132) 2017-04-16 13:27:25 +02:00
Gianluca Barbaro
7d76186798 Mqtt camera (#7092)
* first commit

* minor fixes

* minor fix

* async_camera_image + test

* minor fix

* async calls
2017-04-16 11:06:57 +02:00
Marcelo Moreira de Mello
9249b6bc33 Upgraded Amcrest module to 1.1.9 to support new firmware versions: (#7130)
- V2.400.AC01.15.R.20170328
    - V2.420.AC00.17.R.20170322
2017-04-16 09:05:15 +02:00
Anders Melchiorsen
6cbe28a9cd Send stderr of ping tracker to devnull (#7096)
When pinging an inaccessible device, OS errors like

    ping: sendto: No route to host

can occur. For a ping tracker this is not an error but rather a normal
situation. Thus, it makes sense to hide the error.
2017-04-15 19:00:01 -07:00
Fabian Affolter
f7b6f8e8fb Upgrade chardet to 3.0.2 (#7112) 2017-04-15 18:32:06 -07:00
Adam Mills
35de3a1dc4 Use third-party lib aioautomatic for automatic (#7126) 2017-04-15 18:11:36 -07:00
Robbie Trencheny
815422a886 Merge pull request #7109 from home-assistant/allow-extra-in-zone-config
Allow extra keys in zone config
2017-04-15 16:39:29 -07:00
Fabian Affolter
c43a3efabd Remove globally disabled pylint issue and update docstrings (#7111) 2017-04-15 00:32:04 +02:00
Fabian Affolter
b0ffc55cfa Upgrade speedtest-cli to 1.0.4 (#7105) 2017-04-15 00:28:04 +02:00
Fabian Affolter
cce372ff66 Update file header, add const for defaults, and update log messages (#7110) 2017-04-15 00:26:04 +02:00
Fabian Affolter
5ffda53805 Upgrade aiohttp to 2.0.7 (#7106) 2017-04-15 00:18:39 +02:00
Robbie Trencheny
60f7a1947f Allow extra keys in zone config 2017-04-14 14:31:10 -07:00
Adam Mills
0ca80cc27e No product ids configured should fetch all ids (#7091) 2017-04-14 19:09:21 +02:00
Adam Mills
46352f6de9 Uber version bump (#7100) 2017-04-14 19:08:28 +02:00
Robbie Trencheny
7e3e742938 Merge pull request #7094 from robbiet480/zwave-manufacturer-product-names
Break Z-Wave product name up into manufacturer name and product name
2017-04-13 23:10:40 -07:00
Robbie Trencheny
e5756ba41d Break product name up into manufacturer name and product name 2017-04-13 22:48:59 -07:00
Andrey
b6ee2332f4 Better thread safety in zwave node_entity (#7087)
* Better thread safety

* Update node_entity.py
2017-04-13 23:45:27 +03:00
Patrik
c267326891 Added initial support for IKEA Tradfri Gateway (#7074)
* Added initial support for IKEA Tradfri Gateway

* Pinned requirement

* Fixed lint-errors

* Added file to .coveragerc

* Trying to fix commit

* Fixed requirements_all again

* Minor reorder of code

* Minor reorder of code

* Made the changes suggested by @balloob

* Made the changes suggested by @balloob and removed debug

* Update tradfri.py
2017-04-13 10:04:42 -07:00
John Arild Berentsen
38ad5714cd Add support fo map data from Neato (#6939)
* Responsiveness

* Delay was not needed as commands does not return until done.

* Add support for cleaning maps and cleaning data

* Hound

* Docstring

* Update requirements

* Review changes

* External lib now returns the raw data.

* debug

* Sensor did not refresh

* Error handling

* Issue warning on connection error

* Update requirements

* Review changes
2017-04-13 07:41:25 -07:00
Colin O'Dell
01c7616147 Bump qnapstats library version to 0.2.4 (#7085) 2017-04-13 07:39:36 -07:00
Gianluca Barbaro
fa65783f39 MQTT: Managing binary payloads (#6976)
* Managing binary payloads

Hello,
background: I wrote a HA camera component that gets the image from a binary payload. I'm testing it with Zanzito (https://play.google.com/store/apps/details?id=it.barbaro.zanzito) and it works apparently well: it gets the image and correctly displays it in the front-end.
But I had to make the changes I'm proposing here: the message was being blocked because the utf-8 decoding failed.
As far as I know, the utf-8 encoding is required for the topic, not for the payload. What I did here was try the utf-8 decoding, but even if unsuccessful, it dispatches the message anyway.
Is there anything else I'm missing?
thanks
Gianluca

* Update __init__.py

* Update __init__.py

* Update __init__.py

* git test - ignore

* Should work

* minor fixes

* updated mqtt/services.yaml

* added two tests, modified threaded subscribe

* removing polymer

* requested changes

* requested changes - minor fix

* security wrap around payload_file_path

* services.yaml updated

* removed file publishing

* minor fix
2017-04-13 07:38:09 -07:00
Thibault Cohen
9a9342ec3f Fix account balance in fido sensor (#7077) 2017-04-13 07:42:48 +02:00
Robbie Trencheny
34cb02177d Bump braviarc version to 0.3.7 (#7078) 2017-04-12 18:22:23 -07:00
Robbie Trencheny
5ba4033651 Merge pull request #6869 from JesseWebDotCom/dev
Exposed more attributes, enabled play_media tv show or season episodes
2017-04-12 15:17:53 -07:00
Andrey
5e18c997f7 Add product_name attribute to zwave nodes. (#7071) 2017-04-12 21:12:37 +03:00
John Arild Berentsen
d63028e44a Add communication data attributes to Zwave node_entity (#6813)
* Add quality attribute to node

* Move quality to node_entity

* adjustments

* Line lenght

* flake8

* Cleanup and add each entry from getNodeStatistics as attribute

* resolve conflict

* Move NETWORK to hass.data

* Test Done

* Cleanup from review

* Resolve conflicts

* Review changes

* Long lines....

* blank line

* import-error-disable

* Update tests Part1... Again

* Hound

* Argh!

* Argh!

* YABBADABBADOOOOOOO!

* Enhance tests

* hound

* Resolve

* resolve tests...
2017-04-12 19:09:29 +02:00
hawk259
f68542ba0d Adding AlarmDecoder platform (#6900)
* Added AlarmDecoder platform

* remove try/catch for generic execption

* Changes for @pvizeli, thanks for the review!

Removed _ prefix from normal function variables
Removed _hass as it will be set via .hass for us
Broke out the three config (socket, serial, usb) and use vol.Any
Added support for USB I think, don't have device, but should work
Removed components dictionary, was form old group all code that didn't work

* Fix hass string handling
2017-04-12 11:35:35 +02:00
Pascal Vizeli
9d20a17642 Lutron. Bugfix callback registration. (#7042)
* Lutron. Bugfix callback registration.

* Change handling to event
2017-04-12 09:52:01 +02:00
Pascal Vizeli
e026717239 Fix handling with register callbacks on added_to_hass (#7067) 2017-04-12 09:51:19 +02:00
Fabian Affolter
f06cff35ff Upgrade paho-mqtt to 1.2.2 (#7066) 2017-04-12 09:48:21 +02:00
sander76
7cb8f49d62 Telegram bot component (incl. webhook and polling platform) (#6913)
* first commit.

* removed pointless string statement

* manually removed  # homeassistant.components.telegram_webhooks from requirements_all.txt

* deleted obsolete file.

* coveragerc abc
2017-04-11 21:10:56 -07:00
Paulus Schoutsen
edf500e66b Upgrade netdisco to 1.0.0rc2 (#7008)
* Upgrade netdisco to 1.0.0rc2

* fix tests
2017-04-11 20:10:02 -07:00
Paulus Schoutsen
72a01b8a90 Speed up aiohttp (#7064) 2017-04-11 19:58:54 -07:00
micw
3c35d5ea58 Fix/slugify with german umlaut ss (#7029)
* more tests for slugify

* Fix german umlauts in slugify

* Update __init__.py
2017-04-11 19:51:07 -07:00
Paulus Schoutsen
4d9e681fc1 Constrain chardet to 2.3 (#7063) 2017-04-11 19:50:43 -07:00
Martin Hjelmare
4e388666b2 Fix mysensors callback (#7057)
* Fix mysensors callback

* All messages was not triggering proper updates. Fix by checking all
  child value types each update.

* Upgrade mysensors dep

* Fix pickle persistence when upgrading.
2017-04-11 19:17:09 -07:00
johanpalmqvist
ed012014bc Add MaryTTS platform (#6988)
* Add MaryTTS platform

* Fix lint error

* Doc link, config and formatting fixes

* Remove stuff not needed with aiohttp2

* Get rid of unnecessary else statement
2017-04-11 22:52:44 +02:00
Paulus Schoutsen
bf6c4604f4 Merge pull request #7050 from home-assistant/release-0-42-3
0.42.3
2017-04-11 09:30:04 -07:00
Pascal Vizeli
c91cf66dec Bugfix slider (#7047)
* Bugfix slider

* Update input_slider.py

* Update input_slider.py
2017-04-11 09:24:25 -07:00
Pascal Vizeli
11125864c6 Bugfix slider (#7047)
* Bugfix slider

* Update input_slider.py

* Update input_slider.py
2017-04-11 09:23:41 -07:00
Pascal Vizeli
7377ce2640 Bugfix wait on start event (#7013)
* Bugfix wait on start event

* Paulus sugestion

* Change handling with stop_track_task

* Add new unittests

* Update test_core.py
2017-04-11 09:15:31 -07:00
Anders Melchiorsen
0013139591 Plug file leak on LIFX unregister (#7031)
* Plug file leak on LIFX unregister

The aiolifx 0.4.4 release closes its socket when the unregister callback is
called. This plugs a file descriptor leak but also means that we must be
careful to not use the device after it goes unavailable.

Also, when a light reappears, it has a new device that must be used.

* Do not test self.available in service calls

The core will learn to handle that.
2017-04-11 09:15:30 -07:00
Fabian Affolter
e3c2d27f4a Fix US states check (fixes #7015) (#7017) 2017-04-11 09:15:30 -07:00
Xorso
f00d721293 Bump pyalarmdotcom to support new version of aiohttp (#7021)
Add an optional extended description…
2017-04-11 09:15:30 -07:00
Paulus Schoutsen
b295451d46 Fix two more instances of JSON parsing synology (#7014)
Add an optional extended description…
2017-04-11 09:15:30 -07:00
Paulus Schoutsen
7a3df037ba Fix Synology camera content type (#7010) 2017-04-11 09:15:30 -07:00
Paulus Schoutsen
a60e8b16c0 Version bump to 0.42.3 2017-04-11 09:14:37 -07:00
Pascal Vizeli
b52cabf2c0 Bugfix wait on start event (#7013)
* Bugfix wait on start event

* Paulus sugestion

* Change handling with stop_track_task

* Add new unittests

* Update test_core.py
2017-04-11 09:09:31 -07:00
Fabian Affolter
cc459e25cc Remove configuration sample (#7048) 2017-04-11 09:05:27 -07:00
Pascal Vizeli
d7ca9e7a66 Make core to look avilable state of device on servicecall (#7045) 2017-04-11 08:59:46 -07:00
Anders Melchiorsen
f099aee69a Plug file leak on LIFX unregister (#7031)
* Plug file leak on LIFX unregister

The aiolifx 0.4.4 release closes its socket when the unregister callback is
called. This plugs a file descriptor leak but also means that we must be
careful to not use the device after it goes unavailable.

Also, when a light reappears, it has a new device that must be used.

* Do not test self.available in service calls

The core will learn to handle that.
2017-04-11 08:58:51 -07:00
David Straub
07bb64815d Missing line name restriction added (fixes #7039) (#7040) 2017-04-11 13:55:42 +02:00
Fabian Affolter
2cfdb44df6 Upgrade sendgrid to 4.0.0 (#7038) 2017-04-11 13:52:47 +02:00
Fabian Affolter
2748bc4165 Upgrade psutil to 5.2.2 (#7037) 2017-04-11 13:52:34 +02:00
micw
f76a4b2806 Feature/min max improvements (#6786)
* Fix #6783, remove a test that makes no sense anymore

* Fix #6784

* Fix typo in docstring

* Fix handling of known->unknown state, extended test, fix lint errors

* Better handling of mismatch in unit of measurement.

Set state to "unkown" and unit of measurement to "ERR" if unit of measurement differs between aggregatet states.
Add entity_id to logged warning.

* Make icon configurable

* Fix typo

* Fix lint

* Fix lint

* Fix lint

* Add option to set entity_id on min_max sensor

* Fix lint logging-not-lazy

* Revert "Add option to set entity_id on min_max sensor"

This reverts commit 4685f26647.

* Revert "Make icon configurable"

This reverts commit fe45aec82d.

* Fixes
2017-04-11 13:52:12 +02:00
pezinek
197db6bded Google TTS can't read percent sign (#6971) (#7030) 2017-04-11 10:27:45 +02:00
happyleavesaoc
aa3ccf16ca update fedex (#7034) 2017-04-11 08:10:31 +02:00
happyleavesaoc
aa91351ff0 update usps version (#7035) 2017-04-11 08:10:07 +02:00
happyleavesaoc
32da163421 bump ups version (#7033) 2017-04-11 08:09:39 +02:00
Adam Mills
ee988dc884 Additional ZWave coverage (#7024)
* Additional ZWave coverage

* setup_platform tests
2017-04-10 16:11:39 -04:00
micw
05eb73a0e3 more tests for slugify (#7027) 2017-04-10 22:51:46 +03:00
Fabian Affolter
89e8e1a4c7 Replace 'vendor_id' with 'arch' (fixes #7003) (#7023) 2017-04-10 18:43:40 +02:00
Paulus Schoutsen
d081e5ab3a Remove deprecated remote classes (#7011)
* Remove deprecated remote classes

* Lint

* Fix tests

* Lint
2017-04-10 09:04:19 -07:00
Fabian Affolter
ab247b0f4d Fix US states check (fixes #7015) (#7017) 2017-04-10 16:13:43 +02:00
Xorso
6cd3758b58 Bump pyalarmdotcom to support new version of aiohttp (#7021)
Add an optional extended description…
2017-04-10 16:13:07 +02:00
Paulus Schoutsen
90e73fda3c Fix two more instances of JSON parsing synology (#7014)
Add an optional extended description…
2017-04-10 10:18:37 +02:00
Paulus Schoutsen
d5e3cd51a5 Fix Synology camera content type (#7010) 2017-04-10 08:19:22 +02:00
Greg Dowling
ecfe0770ed Bump pywemo version. (#7004) 2017-04-09 22:48:18 +01:00
Paulus Schoutsen
6cc5bb0713 Merge pull request #6995 from home-assistant/release-0-42-2
0.42.2
2017-04-09 01:37:18 -07:00
Paulus Schoutsen
f6e819e799 Downgrade aiohttp to 205 (#6994) 2017-04-09 01:36:36 -07:00
Paulus Schoutsen
c42293eb10 Downgrade aiohttp to 205 (#6994) 2017-04-09 01:36:01 -07:00
Paulus Schoutsen
a6dc86fa75 version bump to 0.42.2 2017-04-09 01:31:46 -07:00
Paulus Schoutsen
ba8488d8f1 Make discovery not block start (#6991)
* Make discovery not block start

* Fix tests
2017-04-09 01:05:34 -07:00
Paulus Schoutsen
eb0a9869d8 Upgrade to aiohttp 2.0.6 (#6992) 2017-04-08 18:29:28 -07:00
Paulus Schoutsen
5d3fe83e62 Warn if start takes a long time. (#6975)
* Warn if start takes a long time.

* ps - cleanup

* Tweak message

* Add tests

* Tweak messagE
2017-04-08 14:53:32 -07:00
happyleavesaoc
2277778d8d update gstreamer (#6987) 2017-04-08 14:53:16 -07:00
Marcelo Moreira de Mello
c5d89499fa Bump Amcrest module to 1.1.8 (#6990)
Fixed traceback when calculating SD card percent storage

   self._state = self._camera.percent(sd_used[0], sd_total[0])
AttributeError: 'Http' object has no attribute 'percent'
2017-04-08 14:52:10 -07:00
Adam Mills
31da54d530 Add tests for ZWaveDeviceEntityValues helper (#6978)
* Add tests for ZWaveDeviceEntityValues helper

* Add remaining coverage
2017-04-08 06:34:59 -07:00
Teemu R
475ac52180 switch.tplink: bump pyhs100 version requirement (#6986) 2017-04-08 06:33:25 -07:00
Teemu R
55077b9965 switch.tplink: upgrade to the newest upstream release which adds support for plugs using the newer communication protocol (#6979) 2017-04-07 19:19:11 -07:00
John Mihalic
ad8ee1383c Update Emby for aiohttp v2 (#6981) 2017-04-07 19:17:10 -07:00
Paulus Schoutsen
64174f5763 Fix control+c quitting HASS (#6974) 2017-04-07 21:02:49 +02:00
Adam Mills
df77529bfe Tests for zwave services (#6937)
* Initial tests for zwave services

* Fix linter issues

* Complete zwave service tests
2017-04-07 09:17:23 -04:00
Nate
8cff98d07b From Dusk till Dawn (#6857)
* Added dawn, dusk, noon and midnight to the Sun component

* Created a helper method for the solar events
2017-04-06 22:59:41 -07:00
happyleavesaoc
216c2682f0 Crime Reports sensor (#6966)
* add crimereports

* add crimereports metadata

* implicit interval

* remove zone support
2017-04-06 22:47:03 -07:00
Teemu R
d952a07658 light.yeelight: catch i/o related exceptions from the backend lib (#6952)
Fixes/mitigates problems with #5949 and #6624
2017-04-06 22:41:47 -07:00
viswa-swami
9254e7e862 Foscam Camera: Adding exception handling when fetching the camera image to avoid python exception errors when host is not reachable or rather any url error to camera (#6964)
* Adding exception handling when fetching the camera image to avoid python errors when host is not reachable or any url errors to camera

* Added exception as ConnectionError instead of plain except

* Added exception as ConnectionError instead of plain except. Removed the unused error handle
2017-04-06 22:40:33 -07:00
Andrey
f96e06a2c2 Preserve customize glob order. (#6963)
* Preserve customize glob order.

* add tests
2017-04-06 22:39:35 -07:00
Pascal Vizeli
3e66df50c8 Initial import for HassIO (#6935)
* Initial import for HassIO

* Cleanup api code for views

* First unittest for view

* Add test for edit view

* Finish unittest

* fix addons test

* cleanup service.yaml

* Address first round with ping command

* handle timeout dynamic

* fix lint
2017-04-06 22:19:08 -07:00
Pascal Vizeli
74ac160355 Bugfix time and task coro (#6968)
* Bugfix time and task coro

* fix also other create_task

* fix tests

* fix lint in test
2017-04-06 21:00:58 -07:00
JesseWebDotCom
c20d48c8e0 import order fix 2017-04-06 20:52:56 -04:00
aufano
2ce8c2f80e Fix current_temperature is rounded (#6960)
* Fix current_temperature is rounded

* fix  Unnecessary parens after 'if'
2017-04-06 17:40:59 -07:00
Adam Mills
51dc8b78cc Update kodi for aiohttp2 (#6967) 2017-04-07 00:12:24 +02:00
JesseWebDotCom
90a834cbda String formatting, import order, type check fixes 2017-04-04 22:33:52 -04:00
JesseWebDotCom
b99dd19ad6 Fixed log errors if session username or content rating is blank 2017-04-02 10:51:03 -04:00
JesseWebDotCom
ae21fa9ce1 Fixed setting username and content rating data 2017-03-31 23:20:36 -04:00
JesseWebDotCom
564a01f344 mend
Exposed more attributes, enabled play_media tv show or season episodes
2017-03-31 22:36:37 -04:00
JesseWebDotCom
05bab8c808 Exposed more attributes, enabled play_media tv show or season episodes 2017-03-31 22:19:04 -04:00
199 changed files with 7020 additions and 2916 deletions

View File

@@ -8,6 +8,9 @@ omit =
homeassistant/helpers/signal.py
# omit pieces of code that rely on external devices being present
homeassistant/components/alarmdecoder.py
homeassistant/components/*/alarmdecoder.py
homeassistant/components/apcupsd.py
homeassistant/components/*/apcupsd.py
@@ -94,6 +97,9 @@ omit =
homeassistant/components/*/thinkingcleaner.py
homeassistant/components/tradfri.py
homeassistant/components/*/tradfri.py
homeassistant/components/twilio.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twilio_call.py
@@ -159,6 +165,7 @@ omit =
homeassistant/components/binary_sensor/flic.py
homeassistant/components/binary_sensor/hikvision.py
homeassistant/components/binary_sensor/iss.py
homeassistant/components/binary_sensor/ping.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
homeassistant/components/camera/amcrest.py
@@ -175,7 +182,6 @@ omit =
homeassistant/components/climate/oem.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/config/zwave.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/myq.py
@@ -225,15 +231,18 @@ 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
homeassistant/components/light/osramlightify.py
homeassistant/components/light/rpi_gpio_pwm.py
homeassistant/components/light/piglow.py
homeassistant/components/light/tikteck.py
homeassistant/components/light/tradfri.py
homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/piglow.py
homeassistant/components/light/zengge.py
homeassistant/components/lirc.py
homeassistant/components/lock/nuki.py
@@ -274,6 +283,7 @@ omit =
homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/spotify.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py
@@ -315,6 +325,7 @@ omit =
homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py
homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/amcrest.py
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py
@@ -375,6 +386,7 @@ omit =
homeassistant/components/sensor/onewire.py
homeassistant/components/sensor/openevse.py
homeassistant/components/sensor/openexchangerates.py
homeassistant/components/sensor/opensky.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/pi_hole.py
homeassistant/components/sensor/plex.py
@@ -432,7 +444,7 @@ omit =
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py
homeassistant/components/telegram_webhooks.py
homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py
homeassistant/components/tts/picotts.py
@@ -442,7 +454,6 @@ omit =
homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/zamg.py
homeassistant/components/zeroconf.py
homeassistant/components/zwave/__init__.py
homeassistant/components/zwave/util.py

View File

@@ -8,6 +8,7 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
#ENV INSTALL_OPENZWAVE no
#ENV INSTALL_LIBCEC no
#ENV INSTALL_PHANTOMJS no
#ENV INSTALL_COAP_CLIENT no
VOLUME /config
@@ -21,7 +22,7 @@ RUN virtualization/Docker/setup_docker_prereqs
# Install hass component dependencies
COPY requirements_all.txt requirements_all.txt
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet
# Copy source
COPY . .

View File

@@ -27,7 +27,8 @@ _LOGGER = logging.getLogger(__name__)
ERROR_LOG_FILENAME = 'home-assistant.log'
FIRST_INIT_COMPONENT = set((
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction'))
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction',
'frontend', 'history'))
def from_config_dict(config: Dict[str, Any],

View File

@@ -0,0 +1,119 @@
"""
Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
"""
import asyncio
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarmdecoder import (DATA_AD,
SIGNAL_PANEL_MESSAGE)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Perform the setup for AlarmDecoder alarm panels."""
_LOGGER.debug("AlarmDecoderAlarmPanel: setup")
device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
async_add_devices([device])
return True
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an AlarmDecoder-based alarm panel."""
def __init__(self, name, hass):
"""Initialize the alarm panel."""
self._display = ""
self._name = name
self._state = STATE_UNKNOWN
_LOGGER.debug("AlarmDecoderAlarm: Setting up panel")
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
@callback
def _message_callback(self, message):
if message.alarm_sounding or message.fire_alarm:
if self._state != STATE_ALARM_TRIGGERED:
self._state = STATE_ALARM_TRIGGERED
self.hass.async_add_job(self.async_update_ha_state())
elif message.armed_away:
if self._state != STATE_ALARM_ARMED_AWAY:
self._state = STATE_ALARM_ARMED_AWAY
self.hass.async_add_job(self.async_update_ha_state())
elif message.armed_home:
if self._state != STATE_ALARM_ARMED_HOME:
self._state = STATE_ALARM_ARMED_HOME
self.hass.async_add_job(self.async_update_ha_state())
else:
if self._state != STATE_ALARM_DISARMED:
self._state = STATE_ALARM_DISARMED
self.hass.async_add_job(self.async_update_ha_state())
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def code_format(self):
"""Regex for code format or None if no code is required."""
return '^\\d{4,6}$'
@property
def state(self):
"""Return the state of the device."""
return self._state
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
"""Send disarm command."""
_LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: %s", code)
if code:
_LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: sending %s1",
str(code))
self.hass.data[DATA_AD].send("{!s}1".format(code))
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: %s", code)
if code:
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: sending %s2",
str(code))
self.hass.data[DATA_AD].send("{!s}2".format(code))
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: %s", code)
if code:
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: sending %s3",
str(code))
self.hass.data[DATA_AD].send("{!s}3".format(code))

View File

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

View File

@@ -0,0 +1,171 @@
"""
Support for AlarmDecoder devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alarmdecoder/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
REQUIREMENTS = ['alarmdecoder==0.12.1.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'alarmdecoder'
DATA_AD = 'alarmdecoder'
CONF_DEVICE = 'device'
CONF_DEVICE_TYPE = 'type'
CONF_DEVICE_HOST = 'host'
CONF_DEVICE_PORT = 'port'
CONF_DEVICE_PATH = 'path'
CONF_DEVICE_BAUD = 'baudrate'
CONF_ZONES = 'zones'
CONF_ZONE_NAME = 'name'
CONF_ZONE_TYPE = 'type'
CONF_PANEL_DISPLAY = 'panel_display'
DEFAULT_DEVICE_TYPE = 'socket'
DEFAULT_DEVICE_HOST = 'localhost'
DEFAULT_DEVICE_PORT = 10000
DEFAULT_DEVICE_PATH = '/dev/ttyUSB0'
DEFAULT_DEVICE_BAUD = 115200
DEFAULT_PANEL_DISPLAY = False
DEFAULT_ZONE_TYPE = 'opening'
SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message'
SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away'
SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home'
SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
DEVICE_SOCKET_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'socket',
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port})
DEVICE_SERIAL_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'serial',
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string})
DEVICE_USB_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'usb'})
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_NAME): cv.string,
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA,
DEVICE_SERIAL_SCHEMA,
DEVICE_USB_SCHEMA),
vol.Optional(CONF_PANEL_DISPLAY,
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
}),
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
"""Common setup for AlarmDecoder devices."""
from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
conf = config.get(DOMAIN)
device = conf.get(CONF_DEVICE)
display = conf.get(CONF_PANEL_DISPLAY)
zones = conf.get(CONF_ZONES)
device_type = device.get(CONF_DEVICE_TYPE)
host = DEFAULT_DEVICE_HOST
port = DEFAULT_DEVICE_PORT
path = DEFAULT_DEVICE_PATH
baud = DEFAULT_DEVICE_BAUD
sync_connect = asyncio.Future(loop=hass.loop)
def handle_open(device):
"""Callback for a successful connection."""
_LOGGER.info("Established a connection with the alarmdecoder.")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
sync_connect.set_result(True)
@callback
def stop_alarmdecoder(event):
"""Callback to handle shutdown alarmdecoder."""
_LOGGER.debug("Shutting down alarmdecoder.")
controller.close()
@callback
def handle_message(sender, message):
"""Callback to handle message from alarmdecoder."""
async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message)
def zone_fault_callback(sender, zone):
"""Callback to handle zone fault from alarmdecoder."""
async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone)
def zone_restore_callback(sender, zone):
"""Callback to handle zone restore from alarmdecoder."""
async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone)
controller = False
if device_type == 'socket':
host = device.get(CONF_DEVICE_HOST)
port = device.get(CONF_DEVICE_PORT)
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
elif device_type == 'serial':
path = device.get(CONF_DEVICE_PATH)
baud = device.get(CONF_DEVICE_BAUD)
controller = AlarmDecoder(SerialDevice(interface=path))
elif device_type == 'usb':
AlarmDecoder(USBDevice.find())
return False
controller.on_open += handle_open
controller.on_message += handle_message
controller.on_zone_fault += zone_fault_callback
controller.on_zone_restore += zone_restore_callback
hass.data[DATA_AD] = controller
controller.open(baud)
result = yield from sync_connect
if not result:
return False
hass.async_add_job(async_load_platform(hass, 'alarm_control_panel', DOMAIN,
conf, config))
if zones:
hass.async_add_job(async_load_platform(
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config))
if display:
hass.async_add_job(async_load_platform(hass, 'sensor', DOMAIN,
conf, config))
return True

View File

@@ -17,9 +17,9 @@ from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
MATCH_ALL, URL_API, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_SERVICES,
URL_API_EVENTS, URL_API_SERVICES,
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
__version__)
from homeassistant.exceptions import TemplateError
@@ -48,7 +48,6 @@ def setup(hass, config):
hass.http.register_view(APIEventView)
hass.http.register_view(APIServicesView)
hass.http.register_view(APIDomainServicesView)
hass.http.register_view(APIEventForwardingView)
hass.http.register_view(APIComponentsView)
hass.http.register_view(APITemplateView)
@@ -319,79 +318,6 @@ class APIDomainServicesView(HomeAssistantView):
return self.json(changed_states)
class APIEventForwardingView(HomeAssistantView):
"""View to handle EventForwarding requests."""
url = URL_API_EVENT_FORWARD
name = "api:event-forward"
event_forwarder = None
@asyncio.coroutine
def post(self, request):
"""Setup an event forwarder."""
_LOGGER.warning('Event forwarding is deprecated. '
'Will be removed by 0.43')
hass = request.app['hass']
try:
data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
host = data['host']
api_password = data['api_password']
except KeyError:
return self.json_message("No host or api_password received.",
HTTP_BAD_REQUEST)
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
return self.json_message("Invalid value received for port.",
HTTP_UNPROCESSABLE_ENTITY)
api = rem.API(host, api_password, port)
valid = yield from hass.loop.run_in_executor(
None, api.validate_api)
if not valid:
return self.json_message("Unable to validate API.",
HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is None:
self.event_forwarder = rem.EventForwarder(hass)
self.event_forwarder.async_connect(api)
return self.json_message("Event forwarding setup.")
@asyncio.coroutine
def delete(self, request):
"""Remove event forwarder."""
try:
data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
host = data['host']
except KeyError:
return self.json_message("No host received.", HTTP_BAD_REQUEST)
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
return self.json_message("Invalid value received for port.",
HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is not None:
api = rem.API(host, None, port)
self.event_forwarder.async_disconnect(api)
return self.json_message("Event forwarding cancelled.")
class APIComponentsView(HomeAssistantView):
"""View to handle Components requests."""

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
from homeassistant.const import CONF_PORT
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['PyMata==2.13']
REQUIREMENTS = ['PyMata==2.14']
_LOGGER = logging.getLogger(__name__)
@@ -29,18 +29,25 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config):
"""Setup the Arduino component."""
"""Set up the Arduino component."""
import serial
port = config[DOMAIN][CONF_PORT]
global BOARD
try:
BOARD = ArduinoBoard(config[DOMAIN][CONF_PORT])
BOARD = ArduinoBoard(port)
except (serial.serialutil.SerialException, FileNotFoundError):
_LOGGER.exception("Your port is not accessible.")
_LOGGER.error("Your port %s is not accessible", port)
return False
if BOARD.get_firmata()[1] <= 2:
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.")
return False
try:
if BOARD.get_firmata()[1] <= 2:
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer")
return False
except IndexError:
_LOGGER.warning("The version of the StandardFirmata sketch was not"
"detected. This may lead to side effects")
def stop_arduino(event):
"""Stop the Arduino service."""
@@ -67,25 +74,20 @@ class ArduinoBoard(object):
def set_mode(self, pin, direction, mode):
"""Set the mode and the direction of a given pin."""
if mode == 'analog' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.INPUT,
self._board.ANALOG)
self._board.set_pin_mode(
pin, self._board.INPUT, self._board.ANALOG)
elif mode == 'analog' and direction == 'out':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.ANALOG)
self._board.set_pin_mode(
pin, self._board.OUTPUT, self._board.ANALOG)
elif mode == 'digital' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.INPUT,
self._board.DIGITAL)
self._board.set_pin_mode(
pin, self._board.INPUT, self._board.DIGITAL)
elif mode == 'digital' and direction == 'out':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.DIGITAL)
self._board.set_pin_mode(
pin, self._board.OUTPUT, self._board.DIGITAL)
elif mode == 'pwm':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.PWM)
self._board.set_pin_mode(
pin, self._board.OUTPUT, self._board.PWM)
def get_analog_inputs(self):
"""Get the values from the pins."""

View File

@@ -263,15 +263,23 @@ class AutomationEntity(ToggleEntity):
@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state."""
enable_automation = DEFAULT_INITIAL_STATE
if self._initial_state is not None:
enable_automation = self._initial_state
_LOGGER.debug("Automation %s initial state %s from config "
"initial_state", self.entity_id, enable_automation)
else:
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
enable_automation = state.state == STATE_ON
self._last_triggered = state.attributes.get('last_triggered')
_LOGGER.debug("Automation %s initial state %s from recorder "
"last state %s", self.entity_id,
enable_automation, state)
else:
enable_automation = DEFAULT_INITIAL_STATE
_LOGGER.debug("Automation %s initial state %s from default "
"initial state", self.entity_id,
enable_automation)
if not enable_automation:
return

View File

@@ -0,0 +1,123 @@
"""
Support for AlarmDecoder zone states- represented as binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.alarmdecoder/
"""
import asyncio
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_OPEN, STATE_CLOSED)
from homeassistant.components.alarmdecoder import (ZONE_SCHEMA,
CONF_ZONES,
CONF_ZONE_NAME,
CONF_ZONE_TYPE,
SIGNAL_ZONE_FAULT,
SIGNAL_ZONE_RESTORE)
DEPENDENCIES = ['alarmdecoder']
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup AlarmDecoder binary sensor devices."""
configured_zones = discovery_info[CONF_ZONES]
devices = []
for zone_num in configured_zones:
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME]
device = AlarmDecoderBinarySensor(hass,
zone_num,
zone_name,
zone_type)
devices.append(device)
async_add_devices(devices)
return True
class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor."""
def __init__(self, hass, zone_number, zone_name, zone_type):
"""Initialize the binary_sensor."""
self._zone_number = zone_number
self._zone_type = zone_type
self._state = 0
self._name = zone_name
self._type = zone_type
_LOGGER.debug('AlarmDecoderBinarySensor: Setup up zone: ' + zone_name)
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_FAULT, self._fault_callback)
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback)
@property
def state(self):
"""Return the state of the binary sensor."""
if self._type == 'opening':
return STATE_OPEN if self.is_on else STATE_CLOSED
return STATE_ON if self.is_on else STATE_OFF
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def icon(self):
"""Icon for device by its type."""
if "window" in self._name.lower():
return "mdi:window-open" if self.is_on else "mdi:window-closed"
if self._type == 'smoke':
return "mdi:fire"
return None
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def is_on(self):
"""Return true if sensor is on."""
return self._state == 1
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
@callback
def _fault_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self._state = 1
self.hass.async_add_job(self.async_update_ha_state())
@callback
def _restore_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self._state = 0
self.hass.async_add_job(self.async_update_ha_state())

View File

@@ -1,4 +1,9 @@
"""Contains functionality to use flic buttons as a binary sensor."""
"""
Support to use flic buttons as a binary sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.flic/
"""
import logging
import threading
@@ -11,39 +16,40 @@ from homeassistant.const import (
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
_LOGGER = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 3
CLICK_TYPE_SINGLE = "single"
CLICK_TYPE_DOUBLE = "double"
CLICK_TYPE_HOLD = "hold"
CLICK_TYPE_SINGLE = 'single'
CLICK_TYPE_DOUBLE = 'double'
CLICK_TYPE_HOLD = 'hold'
CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD]
CONF_IGNORED_CLICK_TYPES = "ignored_click_types"
CONF_IGNORED_CLICK_TYPES = 'ignored_click_types'
EVENT_NAME = "flic_click"
EVENT_DATA_NAME = "button_name"
EVENT_DATA_ADDRESS = "button_address"
EVENT_DATA_TYPE = "click_type"
EVENT_DATA_QUEUED_TIME = "queued_time"
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 5551
EVENT_NAME = 'flic_click'
EVENT_DATA_NAME = 'button_name'
EVENT_DATA_ADDRESS = 'button_address'
EVENT_DATA_TYPE = 'click_type'
EVENT_DATA_QUEUED_TIME = 'queued_time'
# Validation of the user's configuration
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default='localhost'): cv.string,
vol.Optional(CONF_PORT, default=5551): cv.port,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list,
[vol.In(CLICK_TYPES)])
vol.Optional(CONF_IGNORED_CLICK_TYPES):
vol.All(cv.ensure_list, [vol.In(CLICK_TYPES)])
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Setup the flic platform."""
"""Set up the flic platform."""
import pyflic
# Initialize flic client responsible for
@@ -55,11 +61,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
try:
client = pyflic.FlicClient(host, port)
except ConnectionRefusedError:
_LOGGER.error("Failed to connect to flic server.")
_LOGGER.error("Failed to connect to flic server")
return
def new_button_callback(address):
"""Setup newly verified button as device in home assistant."""
"""Set up newly verified button as device in Home Assistant."""
setup_button(hass, config, add_entities, client, address)
client.on_new_verified_button = new_button_callback
@@ -74,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def get_info_callback(items):
"""Add entities for already verified buttons."""
addresses = items["bd_addr_of_verified_buttons"] or []
addresses = items['bd_addr_of_verified_buttons'] or []
for address in addresses:
setup_button(hass, config, add_entities, client, address)
@@ -83,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def start_scanning(config, add_entities, client):
"""Start a new flic client for scanning & connceting to new buttons."""
"""Start a new flic client for scanning and connecting to new buttons."""
import pyflic
scan_wizard = pyflic.ScanWizard()
@@ -91,10 +97,10 @@ def start_scanning(config, add_entities, client):
def scan_completed_callback(scan_wizard, result, address, name):
"""Restart scan wizard to constantly check for new buttons."""
if result == pyflic.ScanWizardResult.WizardSuccess:
_LOGGER.info("Found new button (%s)", address)
_LOGGER.info("Found new button %s", address)
elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
_LOGGER.warning("Failed to connect to button (%s). Reason: %s",
address, result)
_LOGGER.warning(
"Failed to connect to button %s. Reason: %s", address, result)
# Restart scan wizard
start_scanning(config, add_entities, client)
@@ -108,7 +114,7 @@ def setup_button(hass, config, add_entities, client, address):
timeout = config.get(CONF_TIMEOUT)
ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
button = FlicButton(hass, client, address, timeout, ignored_click_types)
_LOGGER.info("Connected to button (%s)", address)
_LOGGER.info("Connected to button %s", address)
add_entities([button])
@@ -161,7 +167,7 @@ class FlicButton(BinarySensorDevice):
@property
def name(self):
"""Return the name of the device."""
return "flic_%s" % self.address.replace(":", "")
return 'flic_{}'.format(self.address.replace(':', ''))
@property
def address(self):
@@ -181,21 +187,21 @@ class FlicButton(BinarySensorDevice):
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
return {"address": self.address}
return {'address': self.address}
def _queued_event_check(self, click_type, time_diff):
"""Generate a log message and returns true if timeout exceeded."""
time_string = "{:d} {}".format(
time_diff, "second" if time_diff == 1 else "seconds")
time_diff, 'second' if time_diff == 1 else 'seconds')
if time_diff > self._timeout:
_LOGGER.warning(
"Queued %s dropped for %s. Time in queue was %s.",
"Queued %s dropped for %s. Time in queue was %s",
click_type, self.address, time_string)
return True
else:
_LOGGER.info(
"Queued %s allowed for %s. Time in queue was %s.",
"Queued %s allowed for %s. Time in queue was %s",
click_type, self.address, time_string)
return False
@@ -227,8 +233,8 @@ class FlicButton(BinarySensorDevice):
EVENT_DATA_TYPE: hass_click_type
})
def _connection_status_changed(self, channel,
connection_status, disconnect_reason):
def _connection_status_changed(
self, channel, connection_status, disconnect_reason):
"""Remove device, if button disconnects."""
import pyflic

View File

@@ -0,0 +1,130 @@
"""
Tracks the latency of a host by sending ICMP echo requests (ping).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ping/
"""
import logging
import subprocess
import re
import sys
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME, CONF_HOST
_LOGGER = logging.getLogger(__name__)
ATTR_ROUND_TRIP_TIME_AVG = 'round_trip_time_avg'
ATTR_ROUND_TRIP_TIME_MAX = 'round_trip_time_max'
ATTR_ROUND_TRIP_TIME_MDEV = 'round_trip_time_mdev'
ATTR_ROUND_TRIP_TIME_MIN = 'round_trip_time_min'
CONF_PING_COUNT = 'count'
DEFAULT_NAME = 'Ping Binary sensor'
DEFAULT_PING_COUNT = 5
DEFAULT_SENSOR_CLASS = 'connectivity'
SCAN_INTERVAL = timedelta(minutes=5)
PING_MATCHER = re.compile(
r'(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)\/(?P<mdev>\d+.\d+)')
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PING_COUNT, default=DEFAULT_PING_COUNT): cv.positive_int,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Ping Binary sensor."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
count = config.get(CONF_PING_COUNT)
add_devices([PingBinarySensor(name, PingData(host, count))], True)
class PingBinarySensor(BinarySensorDevice):
"""Representation of a Ping Binary sensor."""
def __init__(self, name, ping):
"""Initialize the Ping Binary sensor."""
self._name = name
self.ping = ping
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def device_class(self):
"""Return the class of this sensor."""
return DEFAULT_SENSOR_CLASS
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.ping.available
@property
def device_state_attributes(self):
"""Return the state attributes of the ICMP checo request."""
if self.ping.data is not False:
return {
ATTR_ROUND_TRIP_TIME_AVG: self.ping.data['avg'],
ATTR_ROUND_TRIP_TIME_MAX: self.ping.data['max'],
ATTR_ROUND_TRIP_TIME_MDEV: self.ping.data['mdev'],
ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'],
}
def update(self):
"""Get the latest data."""
self.ping.update()
class PingData(object):
"""The Class for handling the data retrieval."""
def __init__(self, host, count):
"""Initialize the data object."""
self._ip_address = host
self._count = count
self.data = {}
self.available = False
if sys.platform == 'win32':
self._ping_cmd = [
'ping', '-n', str(self._count), '-w 1000', self._ip_address]
else:
self._ping_cmd = [
'ping', '-n', '-q', '-c', str(self._count), '-W1',
self._ip_address]
def ping(self):
"""Send ICMP echo request and return details if success."""
pinger = subprocess.Popen(
self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
out = pinger.communicate()
match = PING_MATCHER.search(str(out).split('\n')[-1])
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
return {
'min': rtt_min,
'avg': rtt_avg,
'max': rtt_max,
'mdev': rtt_mdev}
except (subprocess.CalledProcessError, AttributeError):
return False
def update(self):
"""Retrieve the latest details from the host."""
self.data = self.ping()
self.available = bool(self.data)

View File

@@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
import pywemo.discovery as discovery
if discovery_info is not None:
location = discovery_info[2]
mac = discovery_info[3]
location = discovery_info['ssdp_description']
mac = discovery_info['mac_address']
device = discovery.device_from_description(location, mac)
if device:
@@ -40,12 +40,14 @@ class WemoBinarySensor(BinarySensorDevice):
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
def _update_callback(self, _device, _params):
"""Called by the wemo device callback to update state."""
def _update_callback(self, _device, _type, _params):
"""Called by the Wemo device callback to update state."""
_LOGGER.info(
'Subscription update for %s',
_device)
self.update()
updated = self.wemo.subscription_update(_type, _params)
self._update(force_update=(not updated))
if not hasattr(self, 'hass'):
return
self.schedule_update_ha_state()
@@ -72,7 +74,11 @@ class WemoBinarySensor(BinarySensorDevice):
def update(self):
"""Update WeMo state."""
self._update(force_update=True)
def _update(self, force_update=True):
try:
self._state = self.wemo.get_state(True)
except AttributeError:
_LOGGER.warning('Could not update status for %s', self.name)
self._state = self.wemo.get_state(force_update)
except AttributeError as err:
_LOGGER.warning('Could not update status for %s (%s)',
self.name, err)

View File

@@ -66,8 +66,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
obj_holidays = getattr(holidays, country)(years=year)
if province:
if province not in obj_holidays.PROVINCES:
_LOGGER.error('There is no province/state %s in country %s',
if province not in obj_holidays.PROVINCES and \
province not in obj_holidays.STATES:
_LOGGER.error("There is no province/state %s in country %s",
province, country)
return False
else:

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_web)
REQUIREMENTS = ['amcrest==1.1.8']
REQUIREMENTS = ['amcrest==1.1.9']
_LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,74 @@
"""
Camera that loads a picture from an MQTT topic.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.mqtt/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_NAME
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic'
DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the Camera."""
topic = config[CONF_TOPIC]
async_add_devices([MqttCamera(config[CONF_NAME], topic)])
class MqttCamera(Camera):
"""MQTT camera."""
def __init__(self, name, topic):
"""Initialize Local File Camera component."""
super().__init__()
self._name = name
self._topic = topic
self._qos = 0
self._last_image = None
@asyncio.coroutine
def async_camera_image(self):
"""Return image response."""
return self._last_image
@property
def name(self):
"""Return the name of this camera."""
return self._name
def async_added_to_hass(self):
"""Subscribe mqtt events.
This method must be run in the event loop and returns a coroutine.
"""
@callback
def message_received(topic, payload, qos):
"""A new MQTT message has been received."""
self._last_image = payload
return mqtt.async_subscribe(
self.hass, self._topic, message_received, self._qos, None)

View File

@@ -0,0 +1,65 @@
"""
Camera that loads a picture from a local file.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.neato/
"""
import logging
from datetime import timedelta
from homeassistant.components.camera import Camera
from homeassistant.components.neato import (
NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN)
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['neato']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Camera."""
dev = []
for robot in hass.data[NEATO_ROBOTS]:
if 'maps' in robot.traits:
dev.append(NeatoCleaningMap(hass, robot))
_LOGGER.debug('Adding robots for cleaning maps %s', dev)
add_devices(dev, True)
class NeatoCleaningMap(Camera):
"""Neato cleaning map for last clean."""
def __init__(self, hass, robot):
"""Initialize Neato cleaning map."""
super().__init__()
self.robot = robot
self._robot_name = self.robot.name + ' Cleaning Map'
self._robot_serial = self.robot.serial
self.neato = hass.data[NEATO_LOGIN]
self._image_url = None
self._image = None
def camera_image(self):
"""Return image response."""
self.update()
return self._image
@Throttle(timedelta(seconds=10))
def update(self):
"""Check the contents of the map list."""
self.neato.update_robots()
image_url = None
map_data = self.hass.data[NEATO_MAP_DATA]
image_url = map_data[self._robot_serial]['maps'][0]['url']
if image_url == self._image_url:
_LOGGER.debug('The map image_url is the same as old')
return
image = self.neato.download_map(image_url)
self._image = image.read()
self._image_url = image_url
@property
def name(self):
"""Return the name of this camera."""
return self._robot_name

View File

@@ -81,7 +81,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
params=query_payload
)
query_resp = yield from query_req.json()
# Skip content type check because Synology doesn't return JSON with
# right content type
query_resp = yield from query_req.json(content_type=None)
auth_path = query_resp['data'][AUTH_API]['path']
camera_api = query_resp['data'][CAMERA_API]['path']
camera_path = query_resp['data'][CAMERA_API]['path']
@@ -127,7 +129,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
_LOGGER.exception("Error on %s", syno_camera_url)
return False
camera_resp = yield from camera_req.json()
camera_resp = yield from camera_req.json(content_type=None)
cameras = camera_resp['data']['cameras']
# add cameras
@@ -172,7 +174,7 @@ def get_session_id(hass, websession, username, password, login_url, timeout):
login_url,
params=auth_payload
)
auth_resp = yield from auth_req.json()
auth_resp = yield from auth_req.json(content_type=None)
return auth_resp['data']['sid']
except (asyncio.TimeoutError, aiohttp.ClientError):

View File

@@ -101,11 +101,17 @@ class ZoneMinderCamera(MjpegCamera):
status_response = zoneminder.get_state(
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
)
if not status_response:
_LOGGER.warning('Could not get status for monitor %i',
self._monitor_id)
return
if status_response['success'] is False:
_LOGGER.warning('Alarm status API call failed for monitor %i',
self._monitor_id)
return
self._is_recording = status_response['status'] == ZM_STATE_ALARM
@property

View File

@@ -180,7 +180,7 @@ class Configurator(object):
# field validation goes here?
callback(call.data.get(ATTR_FIELDS, {}))
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
def _generate_unique_id(self):
"""Generate a unique configurator ID."""

View File

@@ -20,13 +20,13 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
def get_device(values, node_config, **kwargs):
def get_device(hass, values, node_config, **kwargs):
"""Create zwave entity device."""
invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS)
if (values.primary.command_class ==
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
and values.primary.index == 0):
return ZwaveRollershutter(values, invert_buttons)
return ZwaveRollershutter(hass, values, invert_buttons)
elif (values.primary.command_class in [
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
@@ -37,10 +37,11 @@ def get_device(values, node_config, **kwargs):
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Representation of an Zwave roller shutter."""
def __init__(self, values, invert_buttons):
def __init__(self, hass, values, invert_buttons):
"""Initialize the zwave rollershutter."""
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
# pylint: disable=no-member
self._network = hass.data[zwave.ZWAVE_NETWORK]
self._open_id = None
self._close_id = None
self._current_position = None
@@ -90,11 +91,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
def open_cover(self, **kwargs):
"""Move the roller shutter up."""
zwave.NETWORK.manager.pressButton(self._open_id)
self._network.manager.pressButton(self._open_id)
def close_cover(self, **kwargs):
"""Move the roller shutter down."""
zwave.NETWORK.manager.pressButton(self._close_id)
self._network.manager.pressButton(self._close_id)
def set_cover_position(self, position, **kwargs):
"""Move the roller shutter to a specific position."""
@@ -102,7 +103,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
def stop_cover(self, **kwargs):
"""Stop the roller shutter."""
zwave.NETWORK.manager.releaseButton(self._open_id)
self._network.manager.releaseButton(self._open_id)
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):

View File

@@ -159,13 +159,13 @@ def async_setup(hass, config):
tasks2.append(group.Group.async_create_group(hass, 'living room', [
lights[1], switches[0], 'input_select.living_room_preset',
'rollershutter.living_room_window', media_players[1],
'cover.living_room_window', media_players[1],
'scene.romantic_lights']))
tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
lights[0], switches[1], media_players[0],
'input_slider.noise_allowance']))
tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door']))
lights[2], 'cover.kitchen_window', 'lock.kitchen_door']))
tasks2.append(group.Group.async_create_group(hass, 'doors', [
'lock.front_door', 'lock.kitchen_door',
'garage_door.right_garage_door', 'garage_door.left_garage_door']))
@@ -176,8 +176,8 @@ def async_setup(hass, config):
'device_tracker.demo_paulus']))
tasks2.append(group.Group.async_create_group(hass, 'downstairs', [
'group.living_room', 'group.kitchen',
'scene.romantic_lights', 'rollershutter.kitchen_window',
'rollershutter.living_room_window', 'group.doors',
'scene.romantic_lights', 'cover.kitchen_window',
'cover.living_room_window', 'group.doors',
'thermostat.ecobee',
], view=True))

View File

@@ -4,19 +4,20 @@ Support for the Automatic platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.automatic/
"""
import asyncio
from datetime import timedelta
import logging
import re
import requests
import voluptuous as vol
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, ATTR_ATTRIBUTES)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.util import datetime as dt_util
from homeassistant.helpers.event import async_track_time_interval
REQUIREMENTS = ['aioautomatic==0.2.1']
_LOGGER = logging.getLogger(__name__)
@@ -24,129 +25,103 @@ CONF_CLIENT_ID = 'client_id'
CONF_SECRET = 'secret'
CONF_DEVICES = 'devices'
SCOPE = 'scope:location scope:vehicle:profile scope:user:profile scope:trip'
DEFAULT_TIMEOUT = 5
ATTR_ACCESS_TOKEN = 'access_token'
ATTR_EXPIRES_IN = 'expires_in'
ATTR_RESULTS = 'results'
ATTR_VEHICLE = 'vehicle'
ATTR_ENDED_AT = 'ended_at'
ATTR_END_LOCATION = 'end_location'
URL_AUTHORIZE = 'https://accounts.automatic.com/oauth/access_token/'
URL_VEHICLES = 'https://api.automatic.com/vehicle/'
URL_TRIPS = 'https://api.automatic.com/trip/'
_VEHICLE_ID_REGEX = re.compile(
(URL_VEHICLES + '(.*)?[/]$').replace('/', r'\/'))
SCOPE = ['location', 'vehicle:profile', 'trip']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_SECRET): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string])
vol.Optional(CONF_DEVICES, default=None): vol.All(
cv.ensure_list, [cv.string])
})
def setup_scanner(hass, config: dict, see, discovery_info=None):
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Validate the configuration and return an Automatic scanner."""
import aioautomatic
client = aioautomatic.Client(
client_id=config[CONF_CLIENT_ID],
client_secret=config[CONF_SECRET],
client_session=async_get_clientsession(hass),
request_kwargs={'timeout': DEFAULT_TIMEOUT})
try:
AutomaticDeviceScanner(hass, config, see)
except requests.HTTPError as err:
session = yield from client.create_session_from_password(
SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
data = AutomaticData(hass, session, config[CONF_DEVICES], async_see)
except aioautomatic.exceptions.AutomaticError as err:
_LOGGER.error(str(err))
return False
yield from data.update()
return True
class AutomaticDeviceScanner(object):
"""A class representing an Automatic device."""
class AutomaticData(object):
"""A class representing an Automatic cloud service connection."""
def __init__(self, hass, config: dict, see) -> None:
def __init__(self, hass, session, devices, async_see):
"""Initialize the automatic device scanner."""
self.hass = hass
self._devices = config.get(CONF_DEVICES, None)
self._access_token_payload = {
'username': config.get(CONF_USERNAME),
'password': config.get(CONF_PASSWORD),
'client_id': config.get(CONF_CLIENT_ID),
'client_secret': config.get(CONF_SECRET),
'grant_type': 'password',
'scope': SCOPE
}
self._headers = None
self._token_expires = dt_util.now()
self.last_results = {}
self.last_trips = {}
self.see = see
self.devices = devices
self.session = session
self.async_see = async_see
self._update_info()
async_track_time_interval(hass, self.update, timedelta(seconds=30))
track_utc_time_change(self.hass, self._update_info,
second=range(0, 60, 30))
def _update_headers(self):
"""Get the access token from automatic."""
if self._headers is None or self._token_expires <= dt_util.now():
resp = requests.post(
URL_AUTHORIZE,
data=self._access_token_payload)
resp.raise_for_status()
json = resp.json()
access_token = json[ATTR_ACCESS_TOKEN]
self._token_expires = dt_util.now() + timedelta(
seconds=json[ATTR_EXPIRES_IN])
self._headers = {
'Authorization': 'Bearer {}'.format(access_token)
}
def _update_info(self, now=None) -> None:
@asyncio.coroutine
def update(self, now=None):
"""Update the device info."""
import aioautomatic
_LOGGER.debug('Updating devices %s', now)
self._update_headers()
response = requests.get(URL_VEHICLES, headers=self._headers)
try:
vehicles = yield from self.session.get_vehicles()
except aioautomatic.exceptions.AutomaticError as err:
_LOGGER.error(str(err))
return False
response.raise_for_status()
for vehicle in vehicles:
name = vehicle.display_name
if name is None:
name = ' '.join(filter(None, (
str(vehicle.year), vehicle.make, vehicle.model)))
self.last_results = [item for item in response.json()[ATTR_RESULTS]
if self._devices is None or item[
'display_name'] in self._devices]
if self.devices is not None and name not in self.devices:
continue
response = requests.get(URL_TRIPS, headers=self._headers)
self.hass.async_add_job(self.update_vehicle(vehicle, name))
if response.status_code == 200:
for trip in response.json()[ATTR_RESULTS]:
vehicle_id = _VEHICLE_ID_REGEX.match(
trip[ATTR_VEHICLE]).group(1)
if vehicle_id not in self.last_trips:
self.last_trips[vehicle_id] = trip
elif self.last_trips[vehicle_id][ATTR_ENDED_AT] < trip[
ATTR_ENDED_AT]:
self.last_trips[vehicle_id] = trip
@asyncio.coroutine
def update_vehicle(self, vehicle, name):
"""Updated the specified vehicle's data."""
import aioautomatic
for vehicle in self.last_results:
dev_id = vehicle.get('id')
host_name = vehicle.get('display_name')
kwargs = {
'dev_id': vehicle.id,
'host_name': name,
'mac': vehicle.id,
ATTR_ATTRIBUTES: {
'fuel_level': vehicle.fuel_level_percent,
}
}
attrs = {
'fuel_level': vehicle.get('fuel_level_percent')
}
trips = []
try:
# Get the most recent trip for this vehicle
trips = yield from self.session.get_trips(
vehicle=vehicle.id, limit=1)
except aioautomatic.exceptions.AutomaticError as err:
_LOGGER.error(str(err))
kwargs = {
'dev_id': dev_id,
'host_name': host_name,
'mac': dev_id,
ATTR_ATTRIBUTES: attrs
}
if trips:
end_location = trips[0].end_location
kwargs['gps'] = (end_location.lat, end_location.lon)
kwargs['gps_accuracy'] = end_location.accuracy_m
if dev_id in self.last_trips:
end_location = self.last_trips[dev_id][ATTR_END_LOCATION]
kwargs['gps'] = (end_location['lat'], end_location['lon'])
kwargs['gps_accuracy'] = end_location['accuracy_m']
self.see(**kwargs)
yield from self.async_see(**kwargs)

View File

@@ -22,20 +22,20 @@ URL = '/api/locative'
def setup_scanner(hass, config, see, discovery_info=None):
"""Setup an endpoint for the Locative application."""
"""Set up an endpoint for the Locative application."""
hass.http.register_view(LocativeView(see))
return True
class LocativeView(HomeAssistantView):
"""View to handle locative requests."""
"""View to handle Locative requests."""
url = URL
name = 'api:locative'
def __init__(self, see):
"""Initialize Locative url endpoints."""
"""Initialize Locative URL endpoints."""
self.see = see
@asyncio.coroutine
@@ -52,7 +52,6 @@ class LocativeView(HomeAssistantView):
return res
@asyncio.coroutine
# pylint: disable=too-many-return-statements
def _handle(self, hass, data):
"""Handle locative request."""
if 'latitude' not in data or 'longitude' not in data:

View File

@@ -0,0 +1,85 @@
"""
Support for GPS tracking MQTT enabled devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.mqtt_json/
"""
import asyncio
import json
import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt
from homeassistant.core import callback
from homeassistant.components.mqtt import CONF_QOS
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_DEVICES, ATTR_GPS_ACCURACY, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_BATTERY_LEVEL)
DEPENDENCIES = ['mqtt']
_LOGGER = logging.getLogger(__name__)
GPS_JSON_PAYLOAD_SCHEMA = vol.Schema({
vol.Required(ATTR_LATITUDE): vol.Coerce(float),
vol.Required(ATTR_LONGITUDE): vol.Coerce(float),
vol.Optional(ATTR_GPS_ACCURACY, default=None): vol.Coerce(int),
vol.Optional(ATTR_BATTERY_LEVEL, default=None): vol.Coerce(str),
}, extra=vol.ALLOW_EXTRA)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic},
})
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Setup the MQTT tracker."""
devices = config[CONF_DEVICES]
qos = config[CONF_QOS]
dev_id_lookup = {}
@callback
def async_tracker_message_received(topic, payload, qos):
"""MQTT message received."""
dev_id = dev_id_lookup[topic]
try:
data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
except vol.MultipleInvalid:
_LOGGER.error('Skipping update for following data '
'because of missing or malformatted data: %s',
payload)
return
except ValueError:
_LOGGER.error('Error parsing JSON payload: %s', payload)
return
kwargs = _parse_see_args(dev_id, data)
hass.async_add_job(
async_see(**kwargs))
for dev_id, topic in devices.items():
dev_id_lookup[topic] = dev_id
yield from mqtt.async_subscribe(
hass, topic, async_tracker_message_received, qos)
return True
def _parse_see_args(dev_id, data):
"""Parse the payload location parameters, into the format see expects."""
kwargs = {
'gps': (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
'dev_id': dev_id
}
if ATTR_GPS_ACCURACY in data:
kwargs[ATTR_GPS_ACCURACY] = data[ATTR_GPS_ACCURACY]
if ATTR_BATTERY_LEVEL in data:
kwargs['battery'] = data[ATTR_BATTERY_LEVEL]
return kwargs

View File

@@ -20,37 +20,39 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Callback for mysensors platform."""
node = gateway.sensors[msg.node_id]
if node.sketch_name is None:
_LOGGER.info('No sketch_name: node %s', msg.node_id)
_LOGGER.debug('No sketch_name: node %s', msg.node_id)
return
pres = gateway.const.Presentation
set_req = gateway.const.SetReq
for child in node.children.values():
position = child.values.get(set_req.V_POSITION)
if child.type != pres.S_GPS or position is None:
continue
try:
latitude, longitude, _ = position.split(',')
except ValueError:
_LOGGER.error('Payload for V_POSITION %s is not of format '
'latitude,longitude,altitude', position)
continue
name = '{} {} {}'.format(
node.sketch_name, msg.node_id, child.id)
attr = {
mysensors.ATTR_CHILD_ID: child.id,
mysensors.ATTR_DESCRIPTION: child.description,
mysensors.ATTR_DEVICE: gateway.device,
mysensors.ATTR_NODE_ID: msg.node_id,
}
see(
dev_id=slugify(name),
host_name=name,
gps=(latitude, longitude),
battery=node.battery_level,
attributes=attr
)
child = node.children.get(msg.child_id)
if child is None:
return
position = child.values.get(set_req.V_POSITION)
if child.type != pres.S_GPS or position is None:
return
try:
latitude, longitude, _ = position.split(',')
except ValueError:
_LOGGER.error('Payload for V_POSITION %s is not of format '
'latitude,longitude,altitude', position)
return
name = '{} {} {}'.format(
node.sketch_name, msg.node_id, child.id)
attr = {
mysensors.ATTR_CHILD_ID: child.id,
mysensors.ATTR_DESCRIPTION: child.description,
mysensors.ATTR_DEVICE: gateway.device,
mysensors.ATTR_NODE_ID: msg.node_id,
}
see(
dev_id=slugify(name),
host_name=name,
gps=(latitude, longitude),
battery=node.battery_level,
attributes=attr
)
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)

View File

@@ -1,15 +1,8 @@
"""
Tracks devices by sending a ICMP ping.
Tracks devices by sending a ICMP echo request (ping).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ping/
device_tracker:
- platform: ping
count: 2
hosts:
host_one: pc.local
host_two: 192.168.2.25
"""
import logging
import subprocess
@@ -18,14 +11,12 @@ from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL, SOURCE_TYPE_ROUTER)
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant import util
from homeassistant import const
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = []
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
class Host:
class Host(object):
"""Host object with ping detection."""
def __init__(self, ip_address, dev_id, hass, config):
@@ -53,8 +44,10 @@ class Host:
self.ip_address]
def ping(self):
"""Send ICMP ping and return True if success."""
pinger = subprocess.Popen(self._ping_cmd, stdout=subprocess.PIPE)
"""Send an ICMP echo request and return True if success."""
pinger = subprocess.Popen(self._ping_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
try:
pinger.communicate()
return pinger.returncode == 0
@@ -70,7 +63,7 @@ class Host:
return True
failed += 1
_LOGGER.debug("ping KO on ip=%s failed=%d", self.ip_address, failed)
_LOGGER.debug("No response from %s failed=%d", self.ip_address, failed)
def setup_scanner(hass, config, see, discovery_info=None):

View File

@@ -10,6 +10,7 @@ import asyncio
import json
from datetime import timedelta
import logging
import os
import voluptuous as vol
@@ -20,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==0.9.2']
REQUIREMENTS = ['netdisco==1.0.0rc3']
DOMAIN = 'discovery'
@@ -28,11 +29,15 @@ SCAN_INTERVAL = timedelta(seconds=300)
SERVICE_NETGEAR = 'netgear_router'
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HASS_IOS_APP = 'hass_ios'
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
SERVICE_HASSIO = 'hassio'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
SERVICE_WEMO: ('wemo', None),
SERVICE_IKEA_TRADFRI: ('tradfri', None),
SERVICE_HASSIO: ('hassio', None),
'philips_hue': ('light', 'hue'),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
@@ -45,10 +50,10 @@ SERVICE_HANDLERS = {
'denonavr': ('media_player', 'denonavr'),
'samsung_tv': ('media_player', 'samsungtv'),
'yeelight': ('light', 'yeelight'),
'flux_led': ('light', 'flux_led'),
'apple_tv': ('media_player', 'apple_tv'),
'frontier_silicon': ('media_player', 'frontier_silicon'),
'openhome': ('media_player', 'openhome'),
'bose_soundtouch': ('media_player', 'soundtouch'),
}
CONF_IGNORE = 'ignore'
@@ -122,6 +127,10 @@ def async_setup(hass, config):
"""Schedule the first discovery when Home Assistant starts up."""
async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow())
# discovery local services
if 'HASSIO' in os.environ:
hass.async_add_job(new_service_found(SERVICE_HASSIO, {}))
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first)
return True

View File

@@ -3,18 +3,20 @@
FINGERPRINTS = {
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
"frontend.html": "feaf3e9453eca239f29eb10e7645a84f",
"mdi.html": "989f02c51eba561dc32b9ecc628a84b3",
"frontend.html": "1533f44c55927e814294de757cd7eada",
"mdi.html": "1cc8593d3684f7f6f3b3854403216f77",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
"panels/ha-panel-config.html": "39f00f769faa63ee61f1fe6fc85d67f7",
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
"panels/ha-panel-history.html": "6945cebe5d8075aae560d2c4246b063f",
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
"panels/ha-panel-hassio.html": "1d954cfe5f47c4be3cf4f6f5db9a83b2",
"panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
"panels/ha-panel-map.html": "e3c7a54f90dd4269d7e53cdcd96514c6",
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
"panels/ha-panel-zwave.html": "a81f82b48439da80286798558f414a2e",
"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

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

View File

@@ -7,67 +7,53 @@ https://home-assistant.io/components/hassio/
import asyncio
import logging
import os
import re
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPBadGateway
from aiohttp.web_exceptions import (
HTTPBadGateway, HTTPNotFound, HTTPMethodNotAllowed)
from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.components.frontend import register_built_in_panel
DOMAIN = 'hassio'
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
LONG_TASK_TIMEOUT = 900
DEFAULT_TIMEOUT = 10
TIMEOUT = 10
SERVICE_HOST_SHUTDOWN = 'host_shutdown'
SERVICE_HOST_REBOOT = 'host_reboot'
HASSIO_REST_COMMANDS = {
'host/shutdown': ['POST'],
'host/reboot': ['POST'],
'host/update': ['GET'],
'host/info': ['GET'],
'supervisor/info': ['GET'],
'supervisor/update': ['POST'],
'supervisor/options': ['POST'],
'supervisor/reload': ['POST'],
'supervisor/logs': ['GET'],
'homeassistant/info': ['GET'],
'homeassistant/update': ['POST'],
'homeassistant/logs': ['GET'],
'network/info': ['GET'],
'network/options': ['GET'],
}
SERVICE_HOST_UPDATE = 'host_update'
SERVICE_SUPERVISOR_UPDATE = 'supervisor_update'
SERVICE_HOMEASSISTANT_UPDATE = 'homeassistant_update'
SERVICE_ADDON_INSTALL = 'addon_install'
SERVICE_ADDON_UNINSTALL = 'addon_uninstall'
SERVICE_ADDON_UPDATE = 'addon_update'
SERVICE_ADDON_START = 'addon_start'
SERVICE_ADDON_STOP = 'addon_stop'
ATTR_ADDON = 'addon'
ATTR_VERSION = 'version'
SCHEMA_SERVICE_UPDATE = vol.Schema({
vol.Optional(ATTR_VERSION): cv.string,
})
SCHEMA_SERVICE_ADDONS = vol.Schema({
vol.Required(ATTR_ADDON): cv.slug,
})
SCHEMA_SERVICE_ADDONS_VERSION = SCHEMA_SERVICE_ADDONS.extend({
vol.Optional(ATTR_VERSION): cv.string,
})
SERVICE_MAP = {
SERVICE_HOST_SHUTDOWN: None,
SERVICE_HOST_REBOOT: None,
SERVICE_HOST_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_HOMEASSISTANT_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_ADDON_INSTALL: SCHEMA_SERVICE_ADDONS_VERSION,
SERVICE_ADDON_UNINSTALL: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_START: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_STOP: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_UPDATE: SCHEMA_SERVICE_ADDONS_VERSION,
ADDON_REST_COMMANDS = {
'install': ['POST'],
'uninstall': ['POST'],
'start': ['POST'],
'stop': ['POST'],
'update': ['POST'],
'options': ['POST'],
'info': ['GET'],
'logs': ['GET'],
}
@@ -88,63 +74,11 @@ def async_setup(hass, config):
_LOGGER.error("Not connected with HassIO!")
return False
# register base api views
for base in ('host', 'homeassistant'):
hass.http.register_view(HassIOBaseView(hassio, base))
for base in ('supervisor', 'network'):
hass.http.register_view(HassIOBaseEditView(hassio, base))
hass.http.register_view(HassIOView(hassio))
# register view for addons
hass.http.register_view(HassIOAddonsView(hassio))
@asyncio.coroutine
def async_service_handler(service):
"""Handle HassIO service calls."""
addon = service.data.get(ATTR_ADDON)
if ATTR_VERSION in service.data:
version = {ATTR_VERSION: service.data[ATTR_VERSION]}
else:
version = None
# map to api call
if service.service == SERVICE_HOST_UPDATE:
yield from hassio.send_command(
"/host/update", payload=version)
elif service.service == SERVICE_HOST_REBOOT:
yield from hassio.send_command("/host/reboot")
elif service.service == SERVICE_HOST_SHUTDOWN:
yield from hassio.send_command("/host/shutdown")
elif service.service == SERVICE_SUPERVISOR_UPDATE:
yield from hassio.send_command(
"/supervisor/update", payload=version)
elif service.service == SERVICE_HOMEASSISTANT_UPDATE:
yield from hassio.send_command(
"/homeassistant/update", payload=version,
timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_ADDON_INSTALL:
yield from hassio.send_command(
"/addons/{}/install".format(addon), payload=version,
timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_ADDON_UNINSTALL:
yield from hassio.send_command(
"/addons/{}/uninstall".format(addon))
elif service.service == SERVICE_ADDON_START:
yield from hassio.send_command("/addons/{}/start".format(addon))
elif service.service == SERVICE_ADDON_STOP:
yield from hassio.send_command("/addons/{}/stop".format(addon))
elif service.service == SERVICE_ADDON_UPDATE:
yield from hassio.send_command(
"/addons/{}/update".format(addon), payload=version,
timeout=LONG_TASK_TIMEOUT)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service, schema in SERVICE_MAP.items():
hass.services.async_register(
DOMAIN, service, async_service_handler,
descriptions[DOMAIN][service], schema=schema)
if 'frontend' in hass.config.components:
register_built_in_panel(hass, 'hassio', 'Hass.io',
'mdi:access-point-network')
return True
@@ -158,115 +92,122 @@ class HassIO(object):
self.websession = websession
self._ip = ip
@asyncio.coroutine
def is_connected(self):
"""Return True if it connected to HassIO supervisor.
Return a coroutine.
This method is a coroutine.
"""
return self.send_command("/supervisor/ping")
@asyncio.coroutine
def send_command(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT):
"""Send request to API."""
answer = yield from self.send_raw(cmd, payload=payload)
if answer['result'] == 'ok':
return answer['data'] if answer['data'] else True
_LOGGER.error("%s return error %s.", cmd, answer['message'])
return False
@asyncio.coroutine
def send_raw(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT):
"""Send raw request to API."""
try:
with async_timeout.timeout(timeout, loop=self.loop):
with async_timeout.timeout(TIMEOUT, loop=self.loop):
request = yield from self.websession.get(
"http://{}{}".format(self._ip, cmd),
timeout=None, json=payload
"http://{}{}".format(self._ip, "/supervisor/ping")
)
if request.status != 200:
_LOGGER.error("%s return code %d.", cmd, request.status)
return
_LOGGER.error("Ping return code %d.", request.status)
return False
return (yield from request.json())
answer = yield from request.json()
return answer and answer['result'] == 'ok'
except asyncio.TimeoutError:
_LOGGER.error("Timeout on api request %s.", cmd)
_LOGGER.error("Timeout on ping request")
except aiohttp.ClientError:
_LOGGER.error("Client error on api request %s.", cmd)
except aiohttp.ClientError as err:
_LOGGER.error("Client error on ping request %s", err)
return False
@asyncio.coroutine
def command_proxy(self, path, request):
"""Return a client request with proxy origin for HassIO supervisor.
This method is a coroutine.
"""
try:
data = None
headers = None
with async_timeout.timeout(TIMEOUT, loop=self.loop):
data = yield from request.read()
if data:
headers = {CONTENT_TYPE: request.content_type}
else:
data = None
method = getattr(self.websession, request.method.lower())
client = yield from method(
"http://{}/{}".format(self._ip, path), data=data,
headers=headers
)
return client
except aiohttp.ClientError as err:
_LOGGER.error("Client error on api %s request %s.", path, err)
except asyncio.TimeoutError:
_LOGGER.error("Client timeout error on api request %s.", path)
raise HTTPBadGateway()
class HassIOBaseView(HomeAssistantView):
class HassIOView(HomeAssistantView):
"""HassIO view to handle base part."""
name = "api:hassio"
url = "/api/hassio/{path:.+}"
requires_auth = True
def __init__(self, hassio, base):
"""Initialize a hassio base view."""
self.hassio = hassio
self._url_info = "/{}/info".format(base)
self.url = "/api/hassio/{}".format(base)
self.name = "api:hassio:{}".format(base)
@asyncio.coroutine
def get(self, request):
"""Get base data."""
data = yield from self.hassio.send_command(self._url_info)
if not data:
raise HTTPBadGateway()
return web.json_response(data)
class HassIOBaseEditView(HassIOBaseView):
"""HassIO view to handle base with options support."""
def __init__(self, hassio, base):
"""Initialize a hassio base edit view."""
super().__init__(hassio, base)
self._url_options = "/{}/options".format(base)
@asyncio.coroutine
def post(self, request):
"""Set options on host."""
data = yield from request.json()
response = yield from self.hassio.send_raw(
self._url_options, payload=data)
if not response:
raise HTTPBadGateway()
return web.json_response(response)
class HassIOAddonsView(HomeAssistantView):
"""HassIO view to handle addons part."""
requires_auth = True
url = "/api/hassio/addons/{addon}"
name = "api:hassio:addons"
def __init__(self, hassio):
"""Initialize a hassio addon view."""
"""Initialize a hassio base view."""
self.hassio = hassio
@asyncio.coroutine
def get(self, request, addon):
"""Get addon data."""
data = yield from self.hassio.send_command(
"/addons/{}/info".format(addon))
if not data:
raise HTTPBadGateway()
return web.json_response(data)
def _handle(self, request, path):
"""Route data to hassio."""
if path.startswith('addons/'):
parts = path.split('/')
@asyncio.coroutine
def post(self, request, addon):
"""Set options on host."""
data = yield from request.json()
if len(parts) != 3:
raise HTTPNotFound()
response = yield from self.hassio.send_raw(
"/addons/{}/options".format(addon), payload=data)
if not response:
raise HTTPBadGateway()
return web.json_response(response)
allowed_methods = ADDON_REST_COMMANDS.get(parts[-1])
else:
allowed_methods = HASSIO_REST_COMMANDS.get(path)
if allowed_methods is None:
raise HTTPNotFound()
if request.method not in allowed_methods:
raise HTTPMethodNotAllowed(request.method, allowed_methods)
client = yield from self.hassio.command_proxy(path, request)
data = yield from client.read()
if path.endswith('/logs'):
return _create_response_log(client, data)
return _create_response(client, data)
get = _handle
post = _handle
def _create_response(client, data):
"""Convert a response from client request."""
return web.Response(
body=data,
status=client.status,
content_type=client.content_type,
)
def _create_response_log(client, data):
"""Convert a response from client request."""
# Remove color codes
log = re.sub(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", data.decode())
return web.Response(
text=log,
status=client.status,
content_type=CONTENT_TYPE_TEXT_PLAIN,
)

View File

@@ -174,8 +174,8 @@ class InputSlider(Entity):
state = yield from async_get_last_state(self.hass, self.entity_id)
value = state and float(state.state)
# Check against False because value can be 0
if value is not False and self._minimum < value < self._maximum:
# Check against None because value can be 0
if value is not None and self._minimum <= value <= self._maximum:
self._current_value = value
else:
self._current_value = self._minimum

View File

@@ -8,6 +8,7 @@ import asyncio
import os
import json
import logging
import datetime
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -20,6 +21,8 @@ from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.remote import JSONEncoder
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
HTTP_BAD_REQUEST)
@@ -55,6 +58,8 @@ ATTR_TEXT_INPUT_BEHAVIOR = "textInput"
BEHAVIORS = [ATTR_DEFAULT_BEHAVIOR, ATTR_TEXT_INPUT_BEHAVIOR]
ATTR_LAST_SEEN_AT = "lastSeenAt"
ATTR_DEVICE = "device"
ATTR_PUSH_TOKEN = "pushToken"
ATTR_APP = "app"
@@ -138,7 +143,7 @@ IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA)
IDENTIFY_APP_SCHEMA = vol.Schema({
vol.Required(ATTR_APP_BUNDLE_IDENTIFER): cv.string,
vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int,
vol.Required(ATTR_APP_VERSION_NUMBER): cv.positive_int
vol.Optional(ATTR_APP_VERSION_NUMBER): cv.string
}, extra=vol.ALLOW_EXTRA)
IDENTIFY_APP_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_APP_SCHEMA)
@@ -192,7 +197,7 @@ def _save_config(filename, config):
"""Save configuration."""
try:
with open(filename, "w") as fdesc:
fdesc.write(json.dumps(config))
fdesc.write(json.dumps(config, cls=JSONEncoder))
except (IOError, TypeError) as error:
_LOGGER.error("Saving config file failed: %s", error)
return False
@@ -285,7 +290,7 @@ class iOSIdentifyDeviceView(HomeAssistantView):
try:
req_data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
try:
data = IDENTIFY_SCHEMA(req_data)
@@ -293,6 +298,8 @@ class iOSIdentifyDeviceView(HomeAssistantView):
return self.json_message(humanize_error(request.json, ex),
HTTP_BAD_REQUEST)
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now()
name = data.get(ATTR_DEVICE_ID)
CONFIG_FILE[ATTR_DEVICES][name] = data

View File

@@ -108,20 +108,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
lights.append(light)
light_ips.append(ipaddr)
if discovery_info:
device = {}
# discovery_info: ip address,device id,device type
device['ipaddr'] = discovery_info[0]
device['name'] = discovery_info[1]
# As we don't know protocol and mode set to none to autodetect.
device[CONF_PROTOCOL] = None
device[ATTR_MODE] = None
light = FluxLight(device)
if light.is_valid:
lights.append(light)
light_ips.append(device['ipaddr'])
if not config.get(CONF_AUTOMATIC_ADD, False):
add_devices(lights)
return
@@ -230,9 +216,9 @@ class FluxLight(Light):
(red, green, blue) = self._bulb.getRgb()
self._bulb.setRgb(red, green, blue, brightness=brightness)
elif effect == EFFECT_RANDOM:
self._bulb.setRgb(random.randrange(0, 255),
random.randrange(0, 255),
random.randrange(0, 255))
self._bulb.setRgb(random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255))
elif effect == EFFECT_COLORLOOP:
self._bulb.setPresetPattern(0x25, 50)
elif effect == EFFECT_RED_FADE:

View File

@@ -10,7 +10,6 @@ import os
import random
import socket
from datetime import timedelta
from urllib.parse import urlparse
import voluptuous as vol
@@ -115,11 +114,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
allow_hue_groups = config.get(CONF_ALLOW_HUE_GROUPS)
if discovery_info is not None:
host = urlparse(discovery_info[1]).hostname
if "HASS Bridge" in discovery_info[0]:
if "HASS Bridge" in discovery_info.get('name', ''):
_LOGGER.info('Emulated hue found, will not add')
return False
host = discovery_info.get('host')
else:
host = config.get(CONF_HOST, None)

View File

@@ -10,23 +10,28 @@ import asyncio
import sys
from functools import partial
from datetime import timedelta
import async_timeout
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT)
from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
from homeassistant import util
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_point_in_utc_time
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.2']
REQUIREMENTS = ['aiolifx==0.4.4']
UDP_BROADCAST_PORT = 56700
@@ -35,18 +40,19 @@ BULB_LATENCY = 500
CONF_SERVER = 'server'
ATTR_HSBK = 'hsbk'
BYTE_MAX = 255
SHORT_MAX = 65535
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
SUPPORT_TRANSITION)
SUPPORT_XY_COLOR | SUPPORT_TRANSITION | SUPPORT_EFFECT)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
})
# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the LIFX platform."""
@@ -65,6 +71,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
local_addr=(server_addr, UDP_BROADCAST_PORT))
hass.async_add_job(coro)
lifx_effects.setup(hass, lifx_manager)
return True
@@ -82,8 +91,8 @@ class LIFXManager(object):
"""Callback for newly detected bulb."""
if device.mac_addr in self.entities:
entity = self.entities[device.mac_addr]
_LOGGER.debug("%s register AGAIN", entity.ipaddr)
entity.available = True
entity.device = device
_LOGGER.debug("%s register AGAIN", entity.who)
self.hass.async_add_job(entity.async_update_ha_state())
else:
_LOGGER.debug("%s register NEW", device.ip_addr)
@@ -93,7 +102,7 @@ class LIFXManager(object):
def ready(self, device, msg):
"""Callback that adds the device once all data is retrieved."""
entity = LIFXLight(device)
_LOGGER.debug("%s register READY", entity.ipaddr)
_LOGGER.debug("%s register READY", entity.who)
self.entities[device.mac_addr] = entity
self.async_add_devices([entity])
@@ -102,12 +111,44 @@ class LIFXManager(object):
"""Callback for disappearing bulb."""
if device.mac_addr in self.entities:
entity = self.entities[device.mac_addr]
_LOGGER.debug("%s unregister", entity.ipaddr)
entity.available = False
entity.updated_event.set()
_LOGGER.debug("%s unregister", entity.who)
entity.device = None
self.hass.async_add_job(entity.async_update_ha_state())
class AwaitAioLIFX:
"""Wait for an aiolifx callback and return the message."""
def __init__(self, light):
"""Initialize the wrapper."""
self.light = light
self.device = None
self.message = None
self.event = asyncio.Event()
@callback
def callback(self, device, message):
"""Callback that aiolifx invokes when the response is received."""
self.device = device
self.message = message
self.event.set()
@asyncio.coroutine
def wait(self, method):
"""Call an aiolifx method and wait for its response or a timeout."""
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
return self.message
def convert_rgb_to_hsv(rgb):
"""Convert Home Assistant RGB values to HSV values."""
red, green, blue = [_ / BYTE_MAX for _ in rgb]
@@ -125,32 +166,30 @@ class LIFXLight(Light):
def __init__(self, device):
"""Initialize the light."""
self.device = device
self.updated_event = asyncio.Event()
self.blocker = None
self.effect_data = None
self.postponed_update = None
self._available = True
self._name = device.label
self.set_power(device.power_level)
self.set_color(*device.color)
@property
def available(self):
"""Return the availability of the device."""
return self._available
@available.setter
def available(self, value):
"""Set the availability of the device."""
self._available = value
return self.device is not None
@property
def name(self):
"""Return the name of the device."""
return self.device.label
return self._name
@property
def ipaddr(self):
"""Return the IP address of the device."""
return self.device.ip_addr[0]
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)
@property
def rgb_color(self):
@@ -180,11 +219,21 @@ class LIFXLight(Light):
_LOGGER.debug("is_on: %d", self._power)
return self._power != 0
@property
def effect(self):
"""Return the currently running effect."""
return self.effect_data.effect.name if self.effect_data else None
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_LIFX
@property
def effect_list(self):
"""Return the list of supported effects."""
return lifx_effects.effect_list()
@callback
def update_after_transition(self, now):
"""Request new status after completion of the last transition."""
@@ -215,38 +264,20 @@ class LIFXLight(Light):
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the device on."""
yield from self.stop_effect()
if ATTR_EFFECT in kwargs:
yield from lifx_effects.default_effect(self, **kwargs)
return
if ATTR_TRANSITION in kwargs:
fade = int(kwargs[ATTR_TRANSITION] * 1000)
else:
fade = 0
changed_color = False
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
changed_color = True
else:
hue = self._hue
saturation = self._sat
brightness = self._bri
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
changed_color = True
else:
brightness = self._bri
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
changed_color = True
else:
kelvin = self._kel
hsbk = [hue, saturation, brightness, kelvin]
hsbk, changed_color = self.find_hsbk(**kwargs)
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
self.ipaddr, self._power, fade, *hsbk)
self.who, self._power, fade, *hsbk)
if self._power == 0:
if changed_color:
@@ -265,6 +296,8 @@ class LIFXLight(Light):
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the device off."""
yield from self.stop_effect()
if ATTR_TRANSITION in kwargs:
fade = int(kwargs[ATTR_TRANSITION] * 1000)
else:
@@ -276,21 +309,71 @@ class LIFXLight(Light):
if fade < BULB_LATENCY:
self.set_power(0)
@callback
def got_color(self, device, msg):
"""Callback that gets current power/color status."""
self.set_power(device.power_level)
self.set_color(*device.color)
self.updated_event.set()
@asyncio.coroutine
def async_update(self):
"""Update bulb status (if it is available)."""
_LOGGER.debug("%s async_update", self.ipaddr)
_LOGGER.debug("%s async_update", self.who)
if self.available and self.blocker is None:
self.updated_event.clear()
self.device.get_color(self.got_color)
yield from self.updated_event.wait()
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."""
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]
color_name = kwargs.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
kwargs[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
changed_color = True
else:
hue = self._hue
saturation = self._sat
brightness = self._bri
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
changed_color = True
else:
brightness = self._bri
if ATTR_XY_COLOR in kwargs:
hue, saturation, _ = \
color_util.color_xy_brightness_to_hsv(
*kwargs[ATTR_XY_COLOR],
ibrightness=(brightness // (BYTE_MAX + 1)))
saturation = saturation * (BYTE_MAX + 1)
changed_color = True
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
changed_color = True
else:
kelvin = self._kel
return [[hue, saturation, brightness, kelvin], changed_color]
def set_power(self, power):
"""Set power state value."""

View File

@@ -0,0 +1,338 @@
"""Support for light effects for the LIFX light platform."""
import logging
import asyncio
import random
from os import path
import voluptuous as vol
from homeassistant.components.light import (
DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_ENTITY_ID)
from homeassistant.helpers.service import extract_entity_ids
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_SPREAD = 'spread'
ATTR_CHANGE = 'change'
# aiolifx waveform modes
WAVEFORM_SINE = 1
WAVEFORM_PULSE = 4
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: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
vol.Optional(ATTR_PERIOD, default=1.0): vol.All(vol.Coerce(float),
vol.Range(min=0.05)),
vol.Optional(ATTR_CYCLES, default=1.0): vol.All(vol.Coerce(float),
vol.Range(min=1)),
})
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
vol.Optional(ATTR_PERIOD, default=60): vol.All(vol.Coerce(float),
vol.Clamp(min=1)),
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)),
})
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):
"""Internal func for applying a service."""
entity_ids = extract_entity_ids(hass, service)
if entity_ids:
devices = [entity for entity in lifx_manager.entities.values()
if entity.entity_id in entity_ids]
else:
devices = list(lifx_manager.entities.values())
if devices:
yield from start_effect(hass, devices,
service.service, **service.data)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
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,
}
if service in (SERVICE_EFFECT_BREATHE, SERVICE_EFFECT_PULSE):
data[ATTR_RGB_COLOR] = [
random.randint(1, 127),
random.randint(1, 127),
random.randint(1, 127),
]
data[ATTR_BRIGHTNESS] = 255
yield from light.hass.services.async_call(DOMAIN, service, data)
def effect_list():
"""Return the list of supported effects."""
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:
yield from light.refresh_state()
if not light.device:
self.lights.remove(light)
else:
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.effect_data:
if light.effect_data.effect == self:
if light.device and not light.effect_data.power:
light.device.set_power(False)
yield from asyncio.sleep(0.5)
if light.device:
light.device.set_color(light.effect_data.color)
yield from asyncio.sleep(0.5)
light.effect_data = None
self.lights.remove(light)
def from_poweroff_hsbk(self, light, **kwargs):
"""The initial color when starting from a powered off state."""
return None
class LIFXEffectBreathe(LIFXEffect):
"""Representation of a breathe effect."""
def __init__(self, hass, lights):
"""Initialize the breathe effect."""
super(LIFXEffectBreathe, self).__init__(hass, lights)
self.name = SERVICE_EFFECT_BREATHE
self.waveform = WAVEFORM_SINE
@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."""
period = kwargs[ATTR_PERIOD]
cycles = kwargs[ATTR_CYCLES]
hsbk, _ = light.find_hsbk(**kwargs)
# Start the effect
args = {
'transient': 1,
'color': hsbk,
'period': int(period*1000),
'cycles': cycles,
'duty_cycle': 0,
'waveform': self.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):
"""Initial color is the target color, but no brightness."""
hsbk, _ = light.find_hsbk(**kwargs)
return [hsbk[0], hsbk[1], 0, hsbk[2]]
class LIFXEffectPulse(LIFXEffectBreathe):
"""Representation of a pulse effect."""
def __init__(self, hass, lights):
"""Initialize the pulse effect."""
super(LIFXEffectPulse, self).__init__(hass, lights)
self.name = SERVICE_EFFECT_PULSE
self.waveform = WAVEFORM_PULSE
class LIFXEffectColorloop(LIFXEffect):
"""Representation of a colorloop effect."""
def __init__(self, hass, lights):
"""Initialize the colorloop effect."""
super(LIFXEffectColorloop, self).__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.randint(0, 359)
while self.lights:
hue = (hue + direction*change) % 360
random.shuffle(self.lights)
lhue = hue
transition = int(1000 * random.uniform(period/2, period))
for light in self.lights:
if 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/359*lhue),
int(random.uniform(0.8, 1.0)*65535),
brightness,
4000,
]
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)
def from_poweroff_hsbk(self, light, **kwargs):
"""Start from a random hue."""
return [random.randint(0, 65535), 65535, 0, 4000]
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(LIFXEffectStop, self).__init__(hass, lights)
self.name = SERVICE_EFFECT_STOP
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Do nothing."""
yield None

View File

@@ -0,0 +1,99 @@
lifx_effect_breathe:
description: Run a breathe effect by fading to a color and back.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.kitchen'
brightness:
description: Number between 0..255 indicating brightness when the effect peaks
example: 120
color_name:
description: A human readable color name
example: 'red'
rgb_color:
description: Color for the fade 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_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'
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..255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light
example: 120
period:
description: Duration between color changes (deafult 60)
example: 180
change:
description: Hue movement per period, in degrees on a color wheel (default 20)
example: 45
spread:
description: Maximum hue difference between participating lights, in degrees on a color wheel (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'

View File

@@ -14,12 +14,9 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Lutron lights."""
area_devs = {}
devs = []
for (area_name, device) in hass.data[LUTRON_DEVICES]['light']:
dev = LutronLight(hass, area_name, device,
hass.data[LUTRON_CONTROLLER])
area_devs.setdefault(area_name, []).append(dev)
dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER])
devs.append(dev)
add_devices(devs, True)
@@ -39,10 +36,10 @@ def to_hass_level(level):
class LutronLight(LutronDevice, Light):
"""Representation of a Lutron Light, including dimmable."""
def __init__(self, hass, area_name, lutron_device, controller):
def __init__(self, area_name, lutron_device, controller):
"""Initialize the light."""
self._prev_brightness = None
LutronDevice.__init__(self, hass, area_name, lutron_device, controller)
LutronDevice.__init__(self, area_name, lutron_device, controller)
@property
def supported_features(self):

View File

@@ -42,7 +42,7 @@ class LutronCasetaLight(LutronCasetaDevice, Light):
def turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs and self._device_type == "WallDimmer":
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
else:
brightness = 255

View File

@@ -0,0 +1,121 @@
"""
Support for myStrom Wifi bulbs.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.mystrom/
"""
import logging
import voluptuous as vol
from homeassistant.components.light import (
Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-mystrom==0.3.8']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'myStrom bulb'
SUPPORT_MYSTROM = (SUPPORT_BRIGHTNESS)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_MAC): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the myStrom Light platform."""
from pymystrom import MyStromBulb
from pymystrom.exceptions import MyStromConnectionError
host = config.get(CONF_HOST)
mac = config.get(CONF_MAC)
name = config.get(CONF_NAME)
bulb = MyStromBulb(host, mac)
try:
if bulb.get_status()['type'] != 'rgblamp':
_LOGGER.error("Device %s (%s) is not a myStrom bulb", host, mac)
return False
except MyStromConnectionError:
_LOGGER.warning("myStrom bulb not online")
add_devices([MyStromLight(bulb, name)], True)
class MyStromLight(Light):
"""Representation of the myStrom WiFi Bulb."""
def __init__(self, bulb, name):
"""Initialize the light."""
self._bulb = bulb
self._name = name
self._state = None
self._available = False
self._brightness = 0
self._rgb_color = [0, 0, 0]
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_MYSTROM
@property
def brightness(self):
"""Brightness of the light."""
return self._brightness
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
@property
def is_on(self):
"""Return true if light is on."""
return self._state['on'] if self._state is not None else STATE_UNKNOWN
def turn_on(self, **kwargs):
"""Turn on the light."""
from pymystrom.exceptions import MyStromConnectionError
brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
try:
if not self.is_on:
self._bulb.set_on()
if brightness is not None:
self._bulb.set_color_hsv(0, 0, round(brightness * 100 / 255))
except MyStromConnectionError:
_LOGGER.warning("myStrom bulb not online")
def turn_off(self, **kwargs):
"""Turn off the bulb."""
from pymystrom.exceptions import MyStromConnectionError
try:
self._bulb.set_off()
except MyStromConnectionError:
_LOGGER.warning("myStrom bulb not online")
def update(self):
"""Fetch new state data for this light."""
from pymystrom.exceptions import MyStromConnectionError
try:
self._state = self._bulb.get_status()
self._brightness = int(self._bulb.get_brightness()) * 255 / 100
self._available = True
except MyStromConnectionError:
_LOGGER.warning("myStrom bulb not online")
self._available = False

View File

@@ -0,0 +1,213 @@
"""
Support for LED lights that can be controlled using PWM.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.pwm/
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_TYPE
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pwmled==1.1.1']
_LOGGER = logging.getLogger(__name__)
CONF_LEDS = 'leds'
CONF_DRIVER = 'driver'
CONF_PINS = 'pins'
CONF_FREQUENCY = 'frequency'
CONF_ADDRESS = 'address'
CONF_DRIVER_GPIO = 'gpio'
CONF_DRIVER_PCA9685 = 'pca9685'
CONF_DRIVER_TYPES = [CONF_DRIVER_GPIO, CONF_DRIVER_PCA9685]
CONF_LED_TYPE_SIMPLE = 'simple'
CONF_LED_TYPE_RGB = 'rgb'
CONF_LED_TYPE_RGBW = 'rgbw'
CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW]
DEFAULT_COLOR = [255, 255, 255]
SUPPORT_SIMPLE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_LEDS): vol.All(cv.ensure_list, [
{
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES),
vol.Required(CONF_PINS): vol.All(cv.ensure_list,
[cv.positive_int]),
vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES),
vol.Optional(CONF_FREQUENCY): cv.positive_int,
vol.Optional(CONF_ADDRESS): cv.byte
}
])
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the pwm lights."""
from pwmled.led import SimpleLed
from pwmled.led.rgb import RgbLed
from pwmled.led.rgbw import RgbwLed
from pwmled.driver.gpio import GpioDriver
from pwmled.driver.pca9685 import Pca9685Driver
leds = []
for led_conf in config[CONF_LEDS]:
driver_type = led_conf[CONF_DRIVER]
pins = led_conf[CONF_PINS]
opt_args = {}
if CONF_FREQUENCY in led_conf:
opt_args['freq'] = led_conf[CONF_FREQUENCY]
if driver_type == CONF_DRIVER_GPIO:
driver = GpioDriver(pins, **opt_args)
elif driver_type == CONF_DRIVER_PCA9685:
if CONF_ADDRESS in led_conf:
opt_args['address'] = led_conf[CONF_ADDRESS]
driver = Pca9685Driver(pins, **opt_args)
else:
_LOGGER.error("Invalid driver type.")
return
name = led_conf[CONF_NAME]
led_type = led_conf[CONF_TYPE]
if led_type == CONF_LED_TYPE_SIMPLE:
led = PwmSimpleLed(SimpleLed(driver), name)
elif led_type == CONF_LED_TYPE_RGB:
led = PwmRgbLed(RgbLed(driver), name)
elif led_type == CONF_LED_TYPE_RGBW:
led = PwmRgbLed(RgbwLed(driver), name)
else:
_LOGGER.error("Invalid led type.")
return
leds.append(led)
add_devices(leds)
class PwmSimpleLed(Light):
"""Representation of a simple on-color pwm led."""
def __init__(self, led, name):
"""Initialize led."""
self._led = led
self._name = name
self._is_on = False
self._brightness = 255
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the group."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._is_on
@property
def brightness(self):
"""Return the brightness property."""
return self._brightness
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_SIMPLE_LED
def turn_on(self, **kwargs):
"""Turn on a led."""
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
if ATTR_TRANSITION in kwargs:
transition_time = kwargs[ATTR_TRANSITION]
self._led.transition(
transition_time,
is_on=True,
brightness=_from_hass_brightness(self._brightness))
else:
self._led.set(is_on=True,
brightness=_from_hass_brightness(self._brightness))
self._is_on = True
self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Turn off a led."""
if self.is_on:
if ATTR_TRANSITION in kwargs:
transition_time = kwargs[ATTR_TRANSITION]
self._led.transition(transition_time, is_on=False)
else:
self._led.off()
self._is_on = False
self.schedule_update_ha_state()
class PwmRgbLed(PwmSimpleLed):
"""Representation of a rgb(w) pwm led."""
def __init__(self, led, name):
"""Initialize led."""
super().__init__(led, name)
self._color = DEFAULT_COLOR
@property
def rgb_color(self):
"""Return the color property."""
return self._color
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_RGB_LED
def turn_on(self, **kwargs):
"""Turn on a led."""
if ATTR_RGB_COLOR in kwargs:
self._color = kwargs[ATTR_RGB_COLOR]
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
if ATTR_TRANSITION in kwargs:
transition_time = kwargs[ATTR_TRANSITION]
self._led.transition(
transition_time,
is_on=True,
brightness=_from_hass_brightness(self._brightness),
color=_from_hass_color(self._color))
else:
self._led.set(is_on=True,
brightness=_from_hass_brightness(self._brightness),
color=_from_hass_color(self._color))
self._is_on = True
self.schedule_update_ha_state()
def _from_hass_brightness(brightness):
"""Convert Home Assistant brightness units to percentage."""
return brightness / 255
def _from_hass_color(color):
"""Convert Home Assistant RGB list to Color tuple."""
from pwmled import Color
return Color(*tuple(color))

View File

@@ -0,0 +1,136 @@
"""Support for the IKEA Tradfri platform."""
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light)
from homeassistant.components.light import \
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
from homeassistant.components.tradfri import KEY_GATEWAY
from homeassistant.util import color as color_util
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['tradfri']
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
IKEA = 'IKEA of Sweden'
ALLOWED_TEMPERATURES = {
IKEA: {2200: 'efd275', 2700: 'f1e0b5', 4000: 'f5faf6'}
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the IKEA Tradfri Light platform."""
if discovery_info is None:
return
gateway_id = discovery_info['gateway']
gateway = hass.data[KEY_GATEWAY][gateway_id]
devices = gateway.get_devices()
lights = [dev for dev in devices if dev.has_light_control]
add_devices(Tradfri(light) for light in lights)
class Tradfri(Light):
"""The platform class required by hass."""
def __init__(self, light):
"""Initialize a Light."""
self._light = light
# Caching of LightControl and light object
self._light_control = light.light_control
self._light_data = light.light_control.lights[0]
self._name = light.name
self._rgb_color = None
self._features = SUPPORT_BRIGHTNESS
if self._light_data.hex_color is not None:
if self._light.device_info.manufacturer == IKEA:
self._features |= SUPPORT_COLOR_TEMP
else:
self._features |= SUPPORT_RGB_COLOR
self._ok_temps = ALLOWED_TEMPERATURES.get(
self._light.device_info.manufacturer)
@property
def supported_features(self):
"""Flag supported features."""
return self._features
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def is_on(self):
"""Return true if light is on."""
return self._light_data.state
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
return self._light_data.dimmer
@property
def color_temp(self):
"""Return the CT color value in mireds."""
if (self._light_data.hex_color is None or
self.supported_features & SUPPORT_COLOR_TEMP == 0 or
not self._ok_temps):
return None
kelvin = next((
kelvin for kelvin, hex_color in self._ok_temps.items()
if hex_color == self._light_data.hex_color), None)
if kelvin is None:
_LOGGER.error(
'unexpected color temperature found for %s: %s',
self.name, self._light_data.hex_color)
return
return color_util.color_temperature_kelvin_to_mired(kelvin)
@property
def rgb_color(self):
"""RGB color of the light."""
return self._rgb_color
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
return self._light_control.set_state(False)
def turn_on(self, **kwargs):
"""
Instruct the light to turn on.
After adding "self._light_data.hexcolor is not None"
for ATTR_RGB_COLOR, this also supports Philips Hue bulbs.
"""
if ATTR_BRIGHTNESS in kwargs:
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS])
else:
self._light_control.set_state(True)
if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None:
self._light.light_control.set_hex_color(
color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR]))
elif ATTR_COLOR_TEMP in kwargs and \
self._light_data.hex_color is not None and self._ok_temps:
kelvin = color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP])
# find closest allowed kelvin temp from user input
kelvin = min(self._ok_temps.keys(), key=lambda x: abs(x - kelvin))
self._light_control.set_hex_color(self._ok_temps[kelvin])
def update(self):
"""Fetch new state data for this light."""
self._light.update()
# Handle Hue lights paired with the gateway
# hex_color is 0 when bulb is unreachable
if self._light_data.hex_color not in (None, '0'):
self._rgb_color = color_util.rgb_hex_to_rgb_list(
self._light_data.hex_color)

View File

@@ -30,8 +30,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
import pywemo.discovery as discovery
if discovery_info is not None:
location = discovery_info[2]
mac = discovery_info[3]
location = discovery_info['ssdp_description']
mac = discovery_info['mac_address']
device = discovery.device_from_description(location, mac)
if device:

View File

@@ -310,25 +310,25 @@ class ZwaveColorLight(ZwaveDimmer):
if self._zw098:
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
self._ct = TEMP_WARM_HASS
rgbw = b'#000000ff00'
rgbw = '#000000ff00'
else:
self._ct = TEMP_COLD_HASS
rgbw = b'#00000000ff'
rgbw = '#00000000ff'
elif ATTR_RGB_COLOR in kwargs:
self._rgb = kwargs[ATTR_RGB_COLOR]
if (not self._zw098 and (
self._color_channels & COLOR_CHANNEL_WARM_WHITE or
self._color_channels & COLOR_CHANNEL_COLD_WHITE)):
rgbw = b'#'
rgbw = '#'
for colorval in color_rgb_to_rgbw(*self._rgb):
rgbw += format(colorval, '02x').encode('utf-8')
rgbw += b'00'
rgbw += format(colorval, '02x')
rgbw += '00'
else:
rgbw = b'#'
rgbw = '#'
for colorval in self._rgb:
rgbw += format(colorval, '02x').encode('utf-8')
rgbw += b'0000'
rgbw += format(colorval, '02x')
rgbw += '0000'
if rgbw and self.values.color:
self.values.color.data = rgbw

View File

@@ -128,11 +128,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
network = hass.data[zwave.ZWAVE_NETWORK]
def set_usercode(service):
"""Set the usercode to index X on the lock."""
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
lock_node = zwave.NETWORK.nodes[node_id]
lock_node = network.nodes[node_id]
code_slot = service.data.get(ATTR_CODE_SLOT)
usercode = service.data.get(ATTR_USERCODE)
@@ -151,7 +152,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def get_usercode(service):
"""Get a usercode at index X on the lock."""
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
lock_node = zwave.NETWORK.nodes[node_id]
lock_node = network.nodes[node_id]
code_slot = service.data.get(ATTR_CODE_SLOT)
for value in lock_node.get_values(
@@ -164,7 +165,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def clear_usercode(service):
"""Set usercode to slot X on the lock."""
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
lock_node = zwave.NETWORK.nodes[node_id]
lock_node = network.nodes[node_id]
code_slot = service.data.get(ATTR_CODE_SLOT)
data = ''

View File

@@ -4,6 +4,7 @@ Component for interacting with a Lutron RadioRA 2 system.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/lutron/
"""
import asyncio
import logging
from homeassistant.helpers import discovery
@@ -50,16 +51,19 @@ def setup(hass, base_config):
class LutronDevice(Entity):
"""Representation of a Lutron device entity."""
def __init__(self, hass, area_name, lutron_device, controller):
def __init__(self, area_name, lutron_device, controller):
"""Initialize the device."""
self._lutron_device = lutron_device
self._controller = controller
self._area_name = area_name
self.hass = hass
self.object_id = '{} {}'.format(area_name, lutron_device.name)
self._controller.subscribe(self._lutron_device, self._update_callback)
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
self.hass.async_add_job(
self._controller.subscribe, self._lutron_device,
self._update_callback
)
def _update_callback(self, _device):
"""Callback invoked by pylutron when the device state changes."""
@@ -68,7 +72,7 @@ class LutronDevice(Entity):
@property
def name(self):
"""Return the name of the device."""
return self._lutron_device.name
return "{} {}".format(self._area_name, self._lutron_device.name)
@property
def should_poll(self):

View File

@@ -10,15 +10,13 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_HOST,
CONF_USERNAME,
CONF_PASSWORD)
from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['https://github.com/gurumitts/'
'pylutron-caseta/archive/v0.2.5.zip#'
'pylutron-caseta==v0.2.5']
'pylutron-caseta/archive/v0.2.6.zip#'
'pylutron-caseta==v0.2.6']
_LOGGER = logging.getLogger(__name__)
@@ -28,9 +26,7 @@ DOMAIN = 'lutron_caseta'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
vol.Required(CONF_HOST): cv.string
})
}, extra=vol.ALLOW_EXTRA)
@@ -41,9 +37,7 @@ def setup(hass, base_config):
config = base_config.get(DOMAIN)
hass.data[LUTRON_CASETA_SMARTBRIDGE] = Smartbridge(
hostname=config[CONF_HOST],
username=config[CONF_USERNAME],
password=config[CONF_PASSWORD]
hostname=config[CONF_HOST]
)
if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected():
_LOGGER.error("Unable to connect to Lutron smartbridge at %s",
@@ -71,14 +65,17 @@ class LutronCasetaDevice(Entity):
self._device_id = device["device_id"]
self._device_type = device["type"]
self._device_name = device["name"]
self._device_zone = device["zone"]
self._state = None
self._smartbridge = bridge
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
self._smartbridge.add_subscriber(self._device_id,
self._update_callback)
self.hass.async_add_job(
self._smartbridge.add_subscriber, self._device_id,
self._update_callback
)
def _update_callback(self):
self.schedule_update_ha_state()
@@ -91,7 +88,8 @@ class LutronCasetaDevice(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {'Lutron Integration ID': self._device_id}
attr = {'Device ID': self._device_id,
'Zone ID': self._device_zone}
return attr
@property

View File

@@ -51,7 +51,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if discovery_info is not None:
name = discovery_info['name']
host = discovery_info['host']
login_id = discovery_info['hsgid']
login_id = discovery_info['properties']['hG']
start_off = False
else:
name = config.get(CONF_NAME)
@@ -145,6 +145,8 @@ class AppleTvDevice(MediaPlayerDevice):
@callback
def playstatus_update(self, updater, playing):
"""Print what is currently playing when it changes."""
self._playing = playing
if self.state == STATE_IDLE:
self._artwork_hash = None
elif self._has_playing_media_changed(playing):
@@ -153,7 +155,6 @@ class AppleTvDevice(MediaPlayerDevice):
self._artwork_hash = hashlib.md5(
base.encode('utf-8')).hexdigest()
self._playing = playing
self.hass.async_add_job(self.async_update_ha_state())
def _has_playing_media_changed(self, new_playing):

View File

@@ -21,8 +21,8 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/aparraga/braviarc/archive/0.3.6.zip'
'#braviarc==0.3.6']
'https://github.com/aparraga/braviarc/archive/0.3.7.zip'
'#braviarc==0.3.7']
BRAVIA_CONFIG_FILE = 'bravia.conf'

View File

@@ -51,11 +51,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hosts = []
if discovery_info and discovery_info in KNOWN_HOSTS:
return
if discovery_info:
host = (discovery_info.get('host'), discovery_info.get('port'))
elif discovery_info:
hosts = [discovery_info]
if host in KNOWN_HOSTS:
return
hosts = [host]
elif CONF_HOST in config:
hosts = [(config.get(CONF_HOST), DEFAULT_PORT)]

View File

@@ -64,8 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.info("Denon receiver at host %s initialized", host)
# 2. option: discovery using netdisco
if discovery_info is not None:
host = discovery_info[0]
name = discovery_info[1]
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)

View File

@@ -37,14 +37,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the DirecTV platform."""
hosts = []
if discovery_info and discovery_info in KNOWN_HOSTS:
return
if discovery_info:
host = discovery_info.get('host')
if host in KNOWN_HOSTS:
return
if discovery_info is not None:
hosts.append([
'DirecTV_' + discovery_info[1],
discovery_info[0],
DEFAULT_PORT
'DirecTV_' + discovery_info.get('serial', ''),
host, DEFAULT_PORT
])
elif CONF_HOST in config:

View File

@@ -47,7 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is not None:
add_devices(
[FSAPIDevice(discovery_info, DEFAULT_PASSWORD)],
[FSAPIDevice(discovery_info['ssdp_description'],
DEFAULT_PASSWORD)],
update_before_add=True)
return True

View File

@@ -9,9 +9,9 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PLAY_MEDIA,
SUPPORT_PLAY, SUPPORT_NEXT_TRACK, PLATFORM_SCHEMA, MediaPlayerDevice)
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PAUSE,
SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, SUPPORT_NEXT_TRACK,
PLATFORM_SCHEMA, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
@@ -20,14 +20,13 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['gstreamer-player==1.0.0']
REQUIREMENTS = ['gstreamer-player==1.1.0']
DOMAIN = 'gstreamer'
CONF_PIPELINE = 'pipeline'
SUPPORT_GSTREAMER = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_SEEK | SUPPORT_STOP | \
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_NEXT_TRACK
SUPPORT_GSTREAMER = SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_PAUSE |\
SUPPORT_PLAY_MEDIA | SUPPORT_NEXT_TRACK
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME): cv.string,
@@ -61,7 +60,6 @@ class GstreamerDevice(MediaPlayerDevice):
self._state = STATE_IDLE
self._volume = None
self._duration = None
self._position = None
self._uri = None
self._title = None
self._artist = None
@@ -72,16 +70,11 @@ class GstreamerDevice(MediaPlayerDevice):
self._state = self._player.state
self._volume = self._player.volume
self._duration = self._player.duration
self._position = self._player.position
self._uri = self._player.uri
self._title = self._player.title
self._album = self._player.album
self._artist = self._player.artist
def mute_volume(self, mute):
"""Send the mute command."""
self._player.mute()
def set_volume_level(self, volume):
"""Set the volume level."""
self._player.volume = volume
@@ -93,9 +86,13 @@ class GstreamerDevice(MediaPlayerDevice):
return
self._player.queue(media_id)
def media_seek(self, position):
"""Seek."""
self._player.position = position
def media_play(self):
"""Play."""
self._player.play()
def media_pause(self):
"""Pause."""
self._player.pause()
def media_next_track(self):
"""Next track."""
@@ -121,11 +118,6 @@ class GstreamerDevice(MediaPlayerDevice):
"""Return the volume level."""
return self._volume
@property
def is_volume_muted(self):
"""Volume muted."""
return self._volume == 0
@property
def supported_features(self):
"""Flag media player features that are supported."""
@@ -141,11 +133,6 @@ class GstreamerDevice(MediaPlayerDevice):
"""Duration of current playing media in seconds."""
return self._duration
@property
def media_position(self):
"""Position of current playing media in seconds."""
return self._position
@property
def media_title(self):
"""Media title."""

View File

@@ -8,6 +8,7 @@ import asyncio
from functools import wraps
import logging
import urllib
import re
import aiohttp
import voluptuous as vol
@@ -17,7 +18,7 @@ from homeassistant.components.media_player import (
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO,
MEDIA_TYPE_PLAYLIST)
MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
@@ -76,6 +77,34 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
cv.boolean,
})
SERVICE_ADD_MEDIA = 'kodi_add_to_playlist'
SERVICE_SET_SHUFFLE = 'kodi_set_shuffle'
ATTR_MEDIA_TYPE = 'media_type'
ATTR_MEDIA_NAME = 'media_name'
ATTR_MEDIA_ARTIST_NAME = 'artist_name'
ATTR_MEDIA_ID = 'media_id'
MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required('shuffle_on'): cv.boolean,
})
MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_TYPE): cv.string,
vol.Optional(ATTR_MEDIA_ID): cv.string,
vol.Optional(ATTR_MEDIA_NAME): cv.string,
vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string,
})
SERVICE_TO_METHOD = {
SERVICE_ADD_MEDIA: {
'method': 'async_add_media_to_playlist',
'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA},
SERVICE_SET_SHUFFLE: {
'method': 'async_set_shuffle',
'schema': MEDIA_PLAYER_SET_SHUFFLE_SCHEMA},
}
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@@ -103,6 +132,33 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async_add_devices([entity], update_before_add=True)
@asyncio.coroutine
def async_service_handler(service):
"""Map services to methods on MediaPlayerDevice."""
method = SERVICE_TO_METHOD.get(service.service)
if not method:
return
params = {key: value for key, value in service.data.items()
if key != 'entity_id'}
yield from getattr(entity, method['method'])(**params)
update_tasks = []
if entity.should_poll:
update_coro = entity.async_update_ha_state(True)
update_tasks.append(update_coro)
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
for service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service].get(
'schema', MEDIA_PLAYER_SCHEMA)
hass.services.async_register(
DOMAIN, service, async_service_handler,
description=None, schema=schema)
def cmd(func):
"""Decorator to catch command exceptions."""
@@ -593,6 +649,139 @@ class KodiDevice(MediaPlayerDevice):
if media_type == "CHANNEL":
return self.server.Player.Open(
{"item": {"channelid": int(media_id)}})
elif media_type == "PLAYLIST":
return self.server.Player.Open(
{"item": {"playlistid": int(media_id)}})
else:
return self.server.Player.Open(
{"item": {"file": str(media_id)}})
@asyncio.coroutine
def async_set_shuffle(self, shuffle_on):
"""Set shuffle mode, for the first player."""
if len(self._players) < 1:
raise RuntimeError("Error: No active player.")
yield from self.server.Player.SetShuffle(
{"playerid": self._players[0]['playerid'], "shuffle": shuffle_on})
@asyncio.coroutine
def async_add_media_to_playlist(
self, media_type, media_id=None, media_name='', artist_name=''):
"""Add a media to default playlist (i.e. playlistid=0).
First the media type must be selected, then
the media can be specified in terms of id or
name and optionally artist name.
All the albums of an artist can be added with
media_name="ALL"
"""
if media_type == "SONG":
if media_id is None:
media_id = yield from self.async_find_song(
media_name, artist_name)
yield from self.server.Playlist.Add(
{"playlistid": 0, "item": {"songid": int(media_id)}})
elif media_type == "ALBUM":
if media_id is None:
if media_name == "ALL":
yield from self.async_add_all_albums(artist_name)
return
media_id = yield from self.async_find_album(
media_name, artist_name)
yield from self.server.Playlist.Add(
{"playlistid": 0, "item": {"albumid": int(media_id)}})
else:
raise RuntimeError("Unrecognized media type.")
@asyncio.coroutine
def async_add_all_albums(self, artist_name):
"""Add all albums of an artist to default playlist (i.e. playlistid=0).
The artist is specified in terms of name.
"""
artist_id = yield from self.async_find_artist(artist_name)
albums = yield from self.async_get_albums(artist_id)
for alb in albums['albums']:
yield from self.server.Playlist.Add(
{"playlistid": 0, "item": {"albumid": int(alb['albumid'])}})
@asyncio.coroutine
def async_clear_playlist(self):
"""Clear default playlist (i.e. playlistid=0)."""
return self.server.Playlist.Clear({"playlistid": 0})
@asyncio.coroutine
def async_get_artists(self):
"""Get artists list."""
return (yield from self.server.AudioLibrary.GetArtists())
@asyncio.coroutine
def async_get_albums(self, artist_id=None):
"""Get albums list."""
if artist_id is None:
return (yield from self.server.AudioLibrary.GetAlbums())
else:
return (yield from self.server.AudioLibrary.GetAlbums(
{"filter": {"artistid": int(artist_id)}}))
@asyncio.coroutine
def async_find_artist(self, artist_name):
"""Find artist by name."""
artists = yield from self.async_get_artists()
out = self._find(
artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid']
@asyncio.coroutine
def async_get_songs(self, artist_id=None):
"""Get songs list."""
if artist_id is None:
return (yield from self.server.AudioLibrary.GetSongs())
else:
return (yield from self.server.AudioLibrary.GetSongs(
{"filter": {"artistid": int(artist_id)}}))
@asyncio.coroutine
def async_find_song(self, song_name, artist_name=''):
"""Find song by name and optionally artist name."""
artist_id = None
if artist_name != '':
artist_id = yield from self.async_find_artist(artist_name)
songs = yield from self.async_get_songs(artist_id)
if songs['limits']['total'] == 0:
return None
out = self._find(song_name, [a['label'] for a in songs['songs']])
return songs['songs'][out[0][0]]['songid']
@asyncio.coroutine
def async_find_album(self, album_name, artist_name=''):
"""Find album by name and optionally artist name."""
artist_id = None
if artist_name != '':
artist_id = yield from self.async_find_artist(artist_name)
albums = yield from self.async_get_albums(artist_id)
out = self._find(album_name, [a['label'] for a in albums['albums']])
return albums['albums'][out[0][0]]['albumid']
@staticmethod
def _find(key_word, words):
key_word = key_word.split(' ')
patt = [re.compile(
'(^| )' + k + '( |$)', re.IGNORECASE) for k in key_word]
out = [[i, 0] for i in range(len(words))]
for i in range(len(words)):
mtc = [p.search(words[i]) for p in patt]
rate = [m is not None for m in mtc].count(True)
out[i][1] = rate
return sorted(out, key=lambda out: out[1], reverse=True)

View File

@@ -31,21 +31,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Openhome Platform."""
from openhomedevice.Device import Device
if discovery_info:
_LOGGER.info('Openhome device found, (%s)', discovery_info[0])
device = Device(discovery_info[1])
# if device has already been discovered
if device.Uuid() in [x.unique_id for x in DEVICES]:
return True
device = OpenhomeDevice(hass, device)
add_devices([device], True)
DEVICES.append(device)
if not discovery_info:
return True
name = discovery_info.get('name')
description = discovery_info.get('ssdp_description')
_LOGGER.info('Openhome device found, (%s)', name)
device = Device(description)
# if device has already been discovered
if device.Uuid() in [x.unique_id for x in DEVICES]:
return True
device = OpenhomeDevice(hass, device)
add_devices([device], True)
DEVICES.append(device)
return True

View File

@@ -51,11 +51,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info:
_LOGGER.debug('%s', discovery_info)
vals = discovery_info.split(':')
if len(vals) > 1:
port = vals[1]
host = vals[0]
host = discovery_info.get('host')
port = discovery_info.get('port')
remote = RemoteControl(host, port)
add_devices([PanasonicVieraTVDevice(mac, name, remote)])
return True

View File

@@ -39,7 +39,6 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.loader import get_component
REQUIREMENTS = ['plexapi==2.0.2']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
@@ -102,7 +101,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
# Via discovery
elif discovery_info is not None:
# Parse discovery data
host = urlparse(discovery_info[1]).netloc
host = discovery_info.get('host')
_LOGGER.info('Discovered PLEX server: %s', host)
if host in _CONFIGURING:
@@ -265,6 +264,7 @@ class PlexClient(MediaPlayerDevice):
self._machine_identifier = None
self._make = ''
self._media_content_id = None
self._media_content_rating = None
self._media_content_type = None
self._media_duration = None
self._media_image_url = None
@@ -274,6 +274,7 @@ class PlexClient(MediaPlayerDevice):
self._previous_volume_level = 1 # Used in fake muting
self._session = None
self._session_type = None
self._session_username = None
self._state = STATE_IDLE
self._volume_level = 1 # since we can't retrieve remotely
self._volume_muted = False # since we can't retrieve remotely
@@ -343,6 +344,8 @@ class PlexClient(MediaPlayerDevice):
self._session.viewOffset)
self._media_content_id = self._convert_na_to_none(
self._session.ratingKey)
self._media_content_rating = self._convert_na_to_none(
self._session.contentRating)
else:
self._media_position = None
self._media_content_id = None
@@ -354,6 +357,8 @@ class PlexClient(MediaPlayerDevice):
self._session.player.machineIdentifier)
self._name = self._convert_na_to_none(self._session.player.title)
self._player_state = self._session.player.state
self._session_username = self._convert_na_to_none(
self._session.username)
self._make = self._convert_na_to_none(self._session.player.device)
else:
self._is_player_available = False
@@ -786,7 +791,7 @@ class PlexClient(MediaPlayerDevice):
src['library_name']).get(src['artist_name']).album(
src['album_name']).get(src['track_name'])
elif media_type == 'EPISODE':
media = self._get_episode(
media = self._get_tv_media(
src['library_name'], src['show_name'],
src['season_number'], src['episode_number'])
elif media_type == 'PLAYLIST':
@@ -795,18 +800,31 @@ class PlexClient(MediaPlayerDevice):
media = self.device.server.library.section(
src['library_name']).get(src['video_name'])
if media:
self._client_play_media(media, shuffle=src['shuffle'])
import plexapi.playlist
if (media and media_type == 'EPISODE' and
isinstance(media, plexapi.playlist.Playlist)):
# delete episode playlist after being loaded into a play queue
self._client_play_media(media=media, delete=True,
shuffle=src['shuffle'])
elif media:
self._client_play_media(media=media, shuffle=src['shuffle'])
def _get_episode(self, library_name, show_name, season_number,
episode_number):
"""Find TV episode and return a Plex media object."""
def _get_tv_media(self, library_name, show_name, season_number,
episode_number):
"""Find TV media and return a Plex media object."""
target_season = None
target_episode = None
seasons = self.device.server.library.section(library_name).get(
show_name).seasons()
for season in seasons:
show = self.device.server.library.section(library_name).get(
show_name)
if not season_number:
playlist_name = "{} - {} Episodes".format(
self.entity_id, show_name)
return self.device.server.createPlaylist(
playlist_name, show.episodes())
for season in show.seasons():
if int(season.seasonNumber) == int(season_number):
target_season = season
break
@@ -817,6 +835,12 @@ class PlexClient(MediaPlayerDevice):
str(season_number).zfill(2),
str(episode_number).zfill(2))
else:
if not episode_number:
playlist_name = "{} - {} Season {} Episodes".format(
self.entity_id, show_name, str(season_number))
return self.device.server.createPlaylist(
playlist_name, target_season.episodes())
for episode in target_season.episodes():
if int(episode.index) == int(episode_number):
target_episode = episode
@@ -830,7 +854,7 @@ class PlexClient(MediaPlayerDevice):
return target_episode
def _client_play_media(self, media, **params):
def _client_play_media(self, media, delete=False, **params):
"""Instruct Plex client to play a piece of media."""
if not (self.device and
'playback' in self._device_protocol_capabilities):
@@ -838,10 +862,16 @@ class PlexClient(MediaPlayerDevice):
return
import plexapi.playqueue
server_url = media.server.baseurl.split(':')
playqueue = plexapi.playqueue.PlayQueue.create(self.device.server,
media, **params)
# delete dynamic playlists used to build playqueue (ex. play tv season)
if delete:
media.delete()
self._local_client_control_fix()
server_url = self.device.server.baseurl.split(':')
self.device.sendCommand('playback/playMedia', **dict({
'machineIdentifier':
self.device.server.machineIdentifier,
@@ -854,3 +884,13 @@ class PlexClient(MediaPlayerDevice):
'containerKey':
'/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
}, **params))
@property
def device_state_attributes(self):
"""Return the scene state attributes."""
attr = {}
attr['media_content_rating'] = self._media_content_rating
attr['session_username'] = self._session_username
attr['media_library_name'] = self._app_name
return attr

View File

@@ -42,11 +42,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Roku platform."""
hosts = []
if discovery_info and discovery_info in KNOWN_HOSTS:
return
if discovery_info:
host = discovery_info[0]
if discovery_info is not None:
_LOGGER.debug('Discovered Roku: %s', discovery_info[0])
if host in KNOWN_HOSTS:
return
_LOGGER.debug('Discovered Roku: %s', host)
hosts.append(discovery_info[0])
elif CONF_HOST in config:

View File

@@ -58,7 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
mac = config.get(CONF_MAC)
timeout = config.get(CONF_TIMEOUT)
elif discovery_info is not None:
tv_name, model, host = discovery_info
tv_name = discovery_info.get('name')
model = discovery_info.get('model_name')
host = discovery_info.get('host')
name = "{} ({})".format(tv_name, model)
port = DEFAULT_PORT
timeout = DEFAULT_TIMEOUT

View File

@@ -222,30 +222,39 @@ soundtouch_play_everywhere:
description: Play on all Bose Soundtouch devices
fields:
entity_id:
description: Name of entites that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices
master:
description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices
example: 'media_player.soundtouch_home'
soundtouch_create_zone:
description: Create a multi-room zone
fields:
entity_id:
description: Name of entites that will coordinate the multi-room zone. Platform dependent.
master:
description: Name of the master entity that will coordinate the multi-room zone. Platform dependent.
example: 'media_player.soundtouch_home'
slaves:
description: Name of slaves entities to add to the new zone
example: 'media_player.soundtouch_bedroom'
soundtouch_add_zone_slave:
description: Add a slave to a multi-room zone
fields:
entity_id:
description: Name of entites that will be added to the multi-room zone. Platform dependent.
master:
description: Name of the master entity that is coordinating the multi-room zone. Platform dependent.
example: 'media_player.soundtouch_home'
slaves:
description: Name of slaves entities to add to the existing zone
example: 'media_player.soundtouch_bedroom'
soundtouch_remove_zone_slave:
description: Remove a slave from the multi-room zone
fields:
entity_id:
description: Name of entites that will be remove from the multi-room zone. Platform dependent.
master:
description: Name of the master entity that is coordinating the multi-room zone. Platform dependent.
example: 'media_player.soundtouch_home'
slaves:
description: Name of slaves entities to remove from the existing zone
example: 'media_player.soundtouch_bedroom'

View File

@@ -103,7 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
soco.config.EVENT_ADVERTISE_IP = advertise_addr
if discovery_info:
player = soco.SoCo(discovery_info)
player = soco.SoCo(discovery_info.get('host'))
# if device allready exists by config
if player.uid in [x.unique_id for x in hass.data[DATA_SONOS]]:
@@ -292,7 +292,7 @@ class SonosDevice(MediaPlayerDevice):
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe sonos events."""
self.hass.loop.run_in_executor(None, self._subscribe_to_player_events)
self.hass.async_add_job(self._subscribe_to_player_events)
@property
def should_poll(self):

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
STATE_PAUSED, STATE_PLAYING,
STATE_UNAVAILABLE)
REQUIREMENTS = ['libsoundtouch==0.1.0']
REQUIREMENTS = ['libsoundtouch==0.3.0']
_LOGGER = logging.getLogger(__name__)
@@ -29,33 +29,33 @@ MAP_STATUS = {
"PLAY_STATE": STATE_PLAYING,
"BUFFERING_STATE": STATE_PLAYING,
"PAUSE_STATE": STATE_PAUSED,
"STOp_STATE": STATE_OFF
"STOP_STATE": STATE_OFF
}
DATA_SOUNDTOUCH = "soundtouch"
SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({
'master': cv.entity_id,
vol.Required('master'): cv.entity_id
})
SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({
'master': cv.entity_id,
'slaves': cv.entity_ids
vol.Required('master'): cv.entity_id,
vol.Required('slaves'): cv.entity_ids
})
SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({
'master': cv.entity_id,
'slaves': cv.entity_ids
vol.Required('master'): cv.entity_id,
vol.Required('slaves'): cv.entity_ids
})
SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({
'master': cv.entity_id,
'slaves': cv.entity_ids
vol.Required('master'): cv.entity_id,
vol.Required('slaves'): cv.entity_ids
})
DEFAULT_NAME = 'Bose Soundtouch'
DEFAULT_PORT = 8090
DEVICES = []
SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
@@ -70,180 +70,99 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Bose Soundtouch platform."""
name = config.get(CONF_NAME)
if DATA_SOUNDTOUCH not in hass.data:
hass.data[DATA_SOUNDTOUCH] = []
remote_config = {
'name': 'HomeAssistant',
'description': config.get(CONF_NAME),
'id': 'ha.component.soundtouch',
'port': config.get(CONF_PORT),
'host': config.get(CONF_HOST)
}
if discovery_info:
# Discovery
host = discovery_info["host"]
port = int(discovery_info["port"])
soundtouch_device = SoundTouchDevice(name, remote_config)
DEVICES.append(soundtouch_device)
add_devices([soundtouch_device])
# if device already exists by config
if host in [device.config['host'] for device in
hass.data[DATA_SOUNDTOUCH]]:
return
remote_config = {
'id': 'ha.component.soundtouch',
'host': host,
'port': port
}
soundtouch_device = SoundTouchDevice(None, remote_config)
hass.data[DATA_SOUNDTOUCH].append(soundtouch_device)
add_devices([soundtouch_device])
else:
# Config
name = config.get(CONF_NAME)
remote_config = {
'id': 'ha.component.soundtouch',
'port': config.get(CONF_PORT),
'host': config.get(CONF_HOST)
}
soundtouch_device = SoundTouchDevice(name, remote_config)
hass.data[DATA_SOUNDTOUCH].append(soundtouch_device)
add_devices([soundtouch_device])
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
def service_handle(service):
"""Internal func for applying a service."""
master_device_id = service.data.get('master')
slaves_ids = service.data.get('slaves')
slaves = []
if slaves_ids:
slaves = [device for device in hass.data[DATA_SOUNDTOUCH] if
device.entity_id in slaves_ids]
master = next([device for device in hass.data[DATA_SOUNDTOUCH] if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning("Unable to find master with entity_id:" + str(
master_device_id))
return
if service.service == SERVICE_PLAY_EVERYWHERE:
slaves = [d for d in hass.data[DATA_SOUNDTOUCH] if
d.entity_id != master_device_id]
master.create_zone(slaves)
elif service.service == SERVICE_CREATE_ZONE:
master.create_zone(slaves)
elif service.service == SERVICE_REMOVE_ZONE_SLAVE:
master.remove_zone_slave(slaves)
elif service.service == SERVICE_ADD_ZONE_SLAVE:
master.add_zone_slave(slaves)
hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE,
play_everywhere_service,
service_handle,
descriptions.get(SERVICE_PLAY_EVERYWHERE),
schema=SOUNDTOUCH_PLAY_EVERYWHERE)
hass.services.register(DOMAIN, SERVICE_CREATE_ZONE,
create_zone_service,
service_handle,
descriptions.get(SERVICE_CREATE_ZONE),
schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE,
remove_zone_slave,
service_handle,
descriptions.get(SERVICE_REMOVE_ZONE_SLAVE),
schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE,
add_zone_slave,
service_handle,
descriptions.get(SERVICE_ADD_ZONE_SLAVE),
schema=SOUNDTOUCH_ADD_ZONE_SCHEMA)
def play_everywhere_service(service):
"""
Create a zone (multi-room) and play on all devices.
:param service: Home Assistant service with 'master' data set
:Example:
- service: media_player.soundtouch_play_everywhere
data:
master: media_player.soundtouch_living_room
"""
master_device_id = service.data.get('master')
slaves = [d for d in DEVICES if d.entity_id != master_device_id]
master = next([device for device in DEVICES if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning(
"Unable to find master with entity_id:" + str(master_device_id))
elif not slaves:
_LOGGER.warning("Unable to create zone without slaves")
else:
_LOGGER.info(
"Creating zone with master " + str(master.device.config.name))
master.device.create_zone([slave.device for slave in slaves])
def create_zone_service(service):
"""
Create a zone (multi-room) on a master and play on specified slaves.
At least one master and one slave must be specified
:param service: Home Assistant service with 'master' and 'slaves' data set
:Example:
- service: media_player.soundtouch_create_zone
data:
master: media_player.soundtouch_living_room
slaves:
- media_player.soundtouch_room
- media_player.soundtouch_kitchen
"""
master_device_id = service.data.get('master')
slaves_ids = service.data.get('slaves')
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
master = next([device for device in DEVICES if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning(
"Unable to find master with entity_id:" + master_device_id)
elif not slaves:
_LOGGER.warning("Unable to create zone without slaves")
else:
_LOGGER.info(
"Creating zone with master " + str(master.device.config.name))
master.device.create_zone([slave.device for slave in slaves])
def add_zone_slave(service):
"""
Add slave(s) to and existing zone (multi-room).
Zone must already exist and slaves array can not be empty.
:param service: Home Assistant service with 'master' and 'slaves' data set
:Example:
- service: media_player.soundtouch_add_zone_slave
data:
master: media_player.soundtouch_living_room
slaves:
- media_player.soundtouch_room
"""
master_device_id = service.data.get('master')
slaves_ids = service.data.get('slaves')
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
master = next([device for device in DEVICES if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning(
"Unable to find master with entity_id:" + str(master_device_id))
elif not slaves:
_LOGGER.warning("Unable to find slaves to add")
else:
_LOGGER.info(
"Adding slaves to zone with master " + str(
master.device.config.name))
master.device.add_zone_slave([slave.device for slave in slaves])
def remove_zone_slave(service):
"""
Remove slave(s) from and existing zone (multi-room).
Zone must already exist and slaves array can not be empty.
Note: If removing last slave, the zone will be deleted and you'll have to
create a new one. You will not be able to add a new slave anymore
:param service: Home Assistant service with 'master' and 'slaves' data set
:Example:
- service: media_player.soundtouch_remove_zone_slave
data:
master: media_player.soundtouch_living_room
slaves:
- media_player.soundtouch_room
"""
master_device_id = service.data.get('master')
slaves_ids = service.data.get('slaves')
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
master = next([device for device in DEVICES if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning(
"Unable to find master with entity_id:" + master_device_id)
elif not slaves:
_LOGGER.warning("Unable to find slaves to remove")
else:
_LOGGER.info("Removing slaves from zone with master " +
str(master.device.config.name))
master.device.remove_zone_slave([slave.device for slave in slaves])
class SoundTouchDevice(MediaPlayerDevice):
"""Representation of a SoundTouch Bose device."""
def __init__(self, name, config):
"""Create Soundtouch Entity."""
from libsoundtouch import soundtouch_device
self._name = name
self._device = soundtouch_device(config['host'], config['port'])
if name is None:
self._name = self._device.config.name
else:
self._name = name
self._status = self._device.status()
self._volume = self._device.volume()
self._config = config
@@ -297,7 +216,7 @@ class SoundTouchDevice(MediaPlayerDevice):
self._status = self._device.status()
def turn_on(self):
"""Turn the media player on."""
"""Turn on media player."""
self._device.power_on()
self._status = self._device.status()
@@ -392,3 +311,52 @@ class SoundTouchDevice(MediaPlayerDevice):
self._device.select_preset(preset)
else:
_LOGGER.warning("Unable to find preset with id " + str(media_id))
def create_zone(self, slaves):
"""
Create a zone (multi-room) and play on selected devices.
:param slaves: slaves on which to play
"""
if not slaves:
_LOGGER.warning("Unable to create zone without slaves")
else:
_LOGGER.info(
"Creating zone with master " + str(self.device.config.name))
self.device.create_zone([slave.device for slave in slaves])
def remove_zone_slave(self, slaves):
"""
Remove slave(s) from and existing zone (multi-room).
Zone must already exist and slaves array can not be empty.
Note: If removing last slave, the zone will be deleted and you'll have
to create a new one. You will not be able to add a new slave anymore
:param slaves: slaves to remove from the zone
"""
if not slaves:
_LOGGER.warning("Unable to find slaves to remove")
else:
_LOGGER.info("Removing slaves from zone with master " +
str(self.device.config.name))
self.device.remove_zone_slave([slave.device for slave in slaves])
def add_zone_slave(self, slaves):
"""
Add slave(s) to and existing zone (multi-room).
Zone must already exist and slaves array can not be empty.
:param slaves:slaves to add
"""
if not slaves:
_LOGGER.warning("Unable to find slaves to add")
else:
_LOGGER.info(
"Adding slaves to zone with master " + str(
self.device.config.name))
self.device.add_zone_slave([slave.device for slave in slaves])

View File

@@ -0,0 +1,286 @@
"""
Support for interacting with Spotify Connect.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.spotify/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.loader import get_component
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_VOLUME_SET,
SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA,
MediaPlayerDevice)
from homeassistant.const import (
CONF_NAME, STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
COMMIT = '544614f4b1d508201d363e84e871f86c90aa26b2'
REQUIREMENTS = ['https://github.com/happyleavesaoc/spotipy/'
'archive/%s.zip#spotipy==2.4.4' % COMMIT]
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
SUPPORT_SPOTIFY = SUPPORT_VOLUME_SET | SUPPORT_PAUSE | SUPPORT_PLAY |\
SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | SUPPORT_SELECT_SOURCE |\
SUPPORT_PLAY_MEDIA
SCOPE = 'user-read-playback-state user-modify-playback-state'
DEFAULT_CACHE_PATH = '.spotify-token-cache'
AUTH_CALLBACK_PATH = '/api/spotify'
AUTH_CALLBACK_NAME = 'api:spotify'
ICON = 'mdi:spotify'
DEFAULT_NAME = 'Spotify'
DOMAIN = 'spotify'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_CACHE_PATH = 'cache_path'
CONFIGURATOR_LINK_NAME = 'Link Spotify account'
CONFIGURATOR_SUBMIT_CAPTION = 'I authorized successfully'
CONFIGURATOR_DESCRIPTION = 'To link your Spotify account, ' \
'click the link, login, and authorize:'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_CACHE_PATH): cv.string
})
SCAN_INTERVAL = timedelta(seconds=30)
def request_configuration(hass, config, add_devices, oauth):
"""Request Spotify authorization."""
configurator = get_component('configurator')
hass.data[DOMAIN] = configurator.request_config(
hass, DEFAULT_NAME, lambda _: None,
link_name=CONFIGURATOR_LINK_NAME,
link_url=oauth.get_authorize_url(),
description=CONFIGURATOR_DESCRIPTION,
submit_caption=CONFIGURATOR_SUBMIT_CAPTION)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Spotify platform."""
import spotipy.oauth2
callback_url = '{}{}'.format(hass.config.api.base_url, AUTH_CALLBACK_PATH)
cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH))
oauth = spotipy.oauth2.SpotifyOAuth(
config.get(CONF_CLIENT_ID), config.get(CONF_CLIENT_SECRET),
callback_url, scope=SCOPE,
cache_path=cache)
token_info = oauth.get_cached_token()
if not token_info:
_LOGGER.info('no token; requesting authorization')
hass.http.register_view(SpotifyAuthCallbackView(
config, add_devices, oauth))
request_configuration(hass, config, add_devices, oauth)
return
if hass.data.get(DOMAIN):
configurator = get_component('configurator')
configurator.request_done(hass.data.get(DOMAIN))
del hass.data[DOMAIN]
player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME))
add_devices([player], True)
class SpotifyAuthCallbackView(HomeAssistantView):
"""Spotify Authorization Callback View."""
requires_auth = False
url = AUTH_CALLBACK_PATH
name = AUTH_CALLBACK_NAME
def __init__(self, config, add_devices, oauth):
"""Initialize."""
self.config = config
self.add_devices = add_devices
self.oauth = oauth
@callback
def get(self, request):
"""Receive authorization token."""
hass = request.app['hass']
self.oauth.get_access_token(request.GET['code'])
hass.async_add_job(setup_platform, hass, self.config, self.add_devices)
class SpotifyMediaPlayer(MediaPlayerDevice):
"""Representation of a Spotify controller."""
def __init__(self, oauth, name):
"""Initialize."""
self._name = name
self._oauth = oauth
self._album = None
self._title = None
self._artist = None
self._uri = None
self._image_url = None
self._state = STATE_UNKNOWN
self._current_device = None
self._devices = None
self._volume = None
self._player = None
self._token_info = self._oauth.get_cached_token()
def refresh_spotify_instance(self):
"""Fetch a new spotify instance."""
import spotipy
token_refreshed = False
need_token = (self._token_info is None or
self._oauth.is_token_expired(self._token_info))
if need_token:
new_token = \
self._oauth.refresh_access_token(
self._token_info['refresh_token'])
self._token_info = new_token
token_refreshed = True
if self._player is None or token_refreshed:
self._player = \
spotipy.Spotify(auth=self._token_info.get('access_token'))
def update(self):
"""Update state and attributes."""
self.refresh_spotify_instance()
# Available devices
devices = self._player.devices().get('devices')
if devices is not None:
self._devices = {device.get('name'): device.get('id')
for device in devices}
# Current playback state
current = self._player.current_playback()
if current is None:
self._state = STATE_IDLE
return
# Track metadata
item = current.get('item')
if item:
self._album = item.get('album').get('name')
self._title = item.get('name')
self._artist = ', '.join([artist.get('name')
for artist in item.get('artists')])
self._uri = current.get('uri')
self._image_url = item.get('album').get('images')[0].get('url')
# Playing state
self._state = STATE_PAUSED
if current.get('is_playing'):
self._state = STATE_PLAYING
device = current.get('device')
if device is None:
self._state = STATE_IDLE
else:
if device.get('volume_percent'):
self._volume = device.get('volume_percent') / 100
if device.get('name'):
self._current_device = device.get('name')
def set_volume_level(self, volume):
"""Set the volume level."""
self._player.volume(int(volume * 100))
def media_next_track(self):
"""Skip to next track."""
self._player.next_track()
def media_previous_track(self):
"""Skip to previous track."""
self._player.previous_track()
def media_play(self):
"""Start or resume playback."""
self._player.start_playback()
def media_pause(self):
"""Pause playback."""
self._player.pause_playback()
def select_source(self, source):
"""Select playback device."""
self._player.transfer_playback(self._devices[source])
def play_media(self, media_type, media_id, **kwargs):
"""Play media."""
kwargs = {}
if media_type == MEDIA_TYPE_MUSIC:
kwargs['uris'] = [media_id]
elif media_type == MEDIA_TYPE_PLAYLIST:
kwargs['context_uri'] = media_id
else:
_LOGGER.error('media type %s is not supported', media_type)
return
if not media_id.startswith('spotify:'):
_LOGGER.error('media id must be spotify uri')
return
self._player.start_playback(**kwargs)
@property
def name(self):
"""Name."""
return self._name
@property
def icon(self):
"""Icon."""
return ICON
@property
def state(self):
"""Playback state."""
return self._state
@property
def volume_level(self):
"""Device volume."""
return self._volume
@property
def source_list(self):
"""Playback devices."""
return list(self._devices.keys())
@property
def source(self):
"""Current playback device."""
return self._current_device
@property
def media_content_id(self):
"""Media URL."""
return self._uri
@property
def media_image_url(self):
"""Media image url."""
return self._image_url
@property
def media_artist(self):
"""Media artist."""
return self._artist
@property
def media_album_name(self):
"""Media album."""
return self._album
@property
def media_title(self):
"""Media title."""
return self._title
@property
def supported_features(self):
"""Media player features that are supported."""
return SUPPORT_SPOTIFY

View File

@@ -29,8 +29,6 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 9000
TIMEOUT = 10
KNOWN_DEVICES = []
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \
@@ -71,18 +69,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
host, port, error)
return False
# Combine it with port to allow multiple servers at the same host
key = "{}:{}".format(ipaddr, port)
# Only add a media server once
if key in KNOWN_DEVICES:
return False
KNOWN_DEVICES.append(key)
_LOGGER.debug("Creating LMS object for %s", ipaddr)
lms = LogitechMediaServer(hass, host, port, username, password)
if lms is False:
return False
players = yield from lms.create_players()
async_add_devices(players)
@@ -173,6 +161,11 @@ class SqueezeBoxDevice(MediaPlayerDevice):
"""Return the name of the device."""
return self._name
@property
def unique_id(self):
"""Return an unique ID."""
return self._id
@property
def state(self):
"""Return the state of the device."""

View File

@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.webostv/
"""
import logging
import asyncio
from datetime import timedelta
from urllib.parse import urlparse
@@ -24,9 +25,7 @@ from homeassistant.const import (
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
'/archive/v0.1.4.zip'
'#pylgtv==0.1.4',
REQUIREMENTS = ['pylgtv==0.1.6',
'websockets==3.2',
'wakeonlan==0.2.2']
@@ -101,7 +100,8 @@ def setup_tv(host, mac, name, customize, config, hass, add_devices):
_LOGGER.warning(
"Connected to LG webOS TV %s but not paired", host)
return
except (OSError, ConnectionClosed):
except (OSError, ConnectionClosed, TypeError,
asyncio.TimeoutError):
_LOGGER.error("Unable to connect to host %s", host)
return
else:
@@ -198,7 +198,8 @@ class LgWebOSDevice(MediaPlayerDevice):
app = self._app_list[source['appId']]
self._source_list[app['title']] = app
except (OSError, ConnectionClosed):
except (OSError, ConnectionClosed, TypeError,
asyncio.TimeoutError):
self._state = STATE_OFF
@property
@@ -240,7 +241,10 @@ class LgWebOSDevice(MediaPlayerDevice):
def media_image_url(self):
"""Image url of current playing media."""
if self._current_source_id in self._app_list:
return self._app_list[self._current_source_id]['largeIcon']
icon = self._app_list[self._current_source_id]['largeIcon']
if not icon.startswith('http'):
icon = self._app_list[self._current_source_id]['icon']
return icon
return None
@property
@@ -256,7 +260,8 @@ class LgWebOSDevice(MediaPlayerDevice):
self._state = STATE_OFF
try:
self._client.power_off()
except (OSError, ConnectionClosed):
except (OSError, ConnectionClosed, TypeError,
asyncio.TimeoutError):
pass
def turn_on(self):

View File

@@ -59,10 +59,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
zone_ignore = config.get(CONF_ZONE_IGNORE)
if discovery_info is not None:
name = discovery_info[0]
model = discovery_info[1]
ctrl_url = discovery_info[2]
desc_url = discovery_info[3]
name = discovery_info.get('name')
model = discovery_info.get('model_name')
ctrl_url = discovery_info.get('control_url')
desc_url = discovery_info.get('description_url')
if ctrl_url in hass.data[KNOWN]:
_LOGGER.info("%s already manually configured", ctrl_url)
return

View File

@@ -158,6 +158,15 @@ class ModbusHub(object):
count,
**kwargs)
def read_input_registers(self, unit, address, count):
"""Read input registers."""
with self._lock:
kwargs = {'unit': unit} if unit else {}
return self._client.read_input_registers(
address,
count,
**kwargs)
def read_holding_registers(self, unit, address, count):
"""Read holding registers."""
with self._lock:

View File

@@ -28,7 +28,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.1']
REQUIREMENTS = ['paho-mqtt==1.2.3']
_LOGGER = logging.getLogger(__name__)
@@ -201,7 +201,8 @@ def publish_template(hass, topic, payload_template, qos=None, retain=None):
@asyncio.coroutine
def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS):
def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS,
encoding='utf-8'):
"""Subscribe to an MQTT topic."""
@callback
def async_mqtt_topic_subscriber(dp_topic, dp_payload, dp_qos):
@@ -209,7 +210,21 @@ def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS):
if not _match_topic(topic, dp_topic):
return
hass.async_run_job(msg_callback, dp_topic, dp_payload, dp_qos)
if encoding is not None:
try:
payload = dp_payload.decode(encoding)
_LOGGER.debug("Received message on %s: %s",
dp_topic, payload)
except (AttributeError, UnicodeDecodeError):
_LOGGER.error("Illegal payload encoding %s from "
"MQTT topic: %s, Payload: %s",
encoding, dp_topic, dp_payload)
return
else:
_LOGGER.debug("Received binary message on %s", dp_topic)
payload = dp_payload
hass.async_run_job(msg_callback, dp_topic, payload, dp_qos)
async_remove = async_dispatcher_connect(
hass, SIGNAL_MQTT_MESSAGE_RECEIVED, async_mqtt_topic_subscriber)
@@ -218,10 +233,12 @@ def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS):
return async_remove
def subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS):
def subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS,
encoding='utf-8'):
"""Subscribe to an MQTT topic."""
async_remove = run_coroutine_threadsafe(
async_subscribe(hass, topic, msg_callback, qos),
async_subscribe(hass, topic, msg_callback,
qos, encoding),
hass.loop
).result()
@@ -372,16 +389,16 @@ def async_setup(hass, config):
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
qos = call.data[ATTR_QOS]
retain = call.data[ATTR_RETAIN]
try:
if payload_template is not None:
if payload_template is not None:
try:
payload = \
template.Template(payload_template, hass).async_render()
except template.jinja2.TemplateError as exc:
_LOGGER.error(
"Unable to publish to '%s': rendering payload template of "
"'%s' failed because %s",
msg_topic, payload_template, exc)
return
except template.jinja2.TemplateError as exc:
_LOGGER.error(
"Unable to publish to '%s': rendering payload template of "
"'%s' failed because %s",
msg_topic, payload_template, exc)
return
yield from hass.data[DATA_MQTT].async_publish(
msg_topic, payload, qos, retain)
@@ -564,18 +581,10 @@ class MQTT(object):
def _mqtt_on_message(self, _mqttc, _userdata, msg):
"""Message received callback."""
try:
payload = msg.payload.decode('utf-8')
except (AttributeError, UnicodeDecodeError):
_LOGGER.error("Illegal utf-8 unicode payload from "
"MQTT topic: %s, Payload: %s", msg.topic,
msg.payload)
else:
_LOGGER.info("Received message on %s: %s", msg.topic, payload)
dispatcher_send(
self.hass, SIGNAL_MQTT_MESSAGE_RECEIVED, msg.topic, payload,
msg.qos
)
dispatcher_send(
self.hass, SIGNAL_MQTT_MESSAGE_RECEIVED, msg.topic, msg.payload,
msg.qos
)
def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos):
"""Unsubscribe successful callback."""

View File

@@ -46,7 +46,7 @@ MYSENSORS_GATEWAYS = 'mysensors_gateways'
MQTT_COMPONENT = 'mqtt'
REQUIREMENTS = [
'https://github.com/theolind/pymysensors/archive/'
'ff3476b70edc9c995b939cddb9d51f8d2d018581.zip#pymysensors==0.9.0']
'c6990eaaa741444a638608e6e00488195e2ca74c.zip#pymysensors==0.9.1']
def is_socket_address(value):
@@ -206,12 +206,9 @@ def setup(hass, config):
for node_id in gateway.sensors:
node = gateway.sensors[node_id]
for child_id in node.children:
child = node.children[child_id]
for value_type in child.values:
msg = mysensors.Message().modify(
node_id=node_id, child_id=child_id, type=1,
sub_type=value_type)
gateway.event_callback(msg)
msg = mysensors.Message().modify(
node_id=node_id, child_id=child_id)
gateway.event_callback(msg)
gateway.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: gateway.stop())
@@ -274,32 +271,33 @@ def pf_callback_factory(map_sv_types, devices, entity_class, add_devices=None):
_LOGGER.debug('No sketch_name: node %s', msg.node_id)
return
child = gateway.sensors[msg.node_id].children.get(msg.child_id)
if child is None or child.values.get(msg.sub_type) is None:
if child is None:
return
key = msg.node_id, child.id, msg.sub_type
if child.type not in map_sv_types or \
msg.sub_type not in map_sv_types[child.type]:
return
if key in devices:
for value_type in child.values:
key = msg.node_id, child.id, value_type
if child.type not in map_sv_types or \
value_type not in map_sv_types[child.type]:
continue
if key in devices:
if add_devices:
devices[key].schedule_update_ha_state(True)
else:
devices[key].update()
continue
name = '{} {} {}'.format(
gateway.sensors[msg.node_id].sketch_name, msg.node_id,
child.id)
if isinstance(entity_class, dict):
device_class = entity_class[child.type]
else:
device_class = entity_class
devices[key] = device_class(
gateway, msg.node_id, child.id, name, value_type)
if add_devices:
devices[key].schedule_update_ha_state(True)
_LOGGER.info('Adding new devices: %s', [devices[key]])
add_devices([devices[key]], True)
else:
devices[key].update()
return
name = '{} {} {}'.format(
gateway.sensors[msg.node_id].sketch_name, msg.node_id,
child.id)
if isinstance(entity_class, dict):
device_class = entity_class[child.type]
else:
device_class = entity_class
devices[key] = device_class(
gateway, msg.node_id, child.id, name, msg.sub_type)
if add_devices:
_LOGGER.info('Adding new devices: %s', [devices[key]])
add_devices([devices[key]], True)
else:
devices[key].update()
return mysensors_callback

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