Compare commits

..

173 Commits
0.32 ... 0.33

Author SHA1 Message Date
Paulus Schoutsen
8b6a94b0f5 Merge pull request #4446 from home-assistant/dev
0.33
2016-11-19 16:06:26 -08:00
Paulus Schoutsen
0a333230c1 Version bump to 0.33 2016-11-19 16:05:56 -08:00
Paulus Schoutsen
455e1df7cb Fix typo 2016-11-19 16:05:33 -08:00
Matt N
f71396c293 Fix nmap_tracker documentation link (#4471) 2016-11-19 15:31:45 -08:00
Paulus Schoutsen
d930c399fe Better locking while setting up components + discovery (#4463) 2016-11-19 08:18:33 -08:00
Bjarni Ivarsson
f3748ce535 Sonos line-in and tv source fixes + Sonos discovery fix. (#4440)
* Fixes line-in and tv sources on Sonos + Sonos discovery fixes.

* Style fix.
2016-11-19 15:29:00 +00:00
John Arild Berentsen
8beefcfc69 Switch did not update (#4466) 2016-11-19 15:52:42 +01:00
John Arild Berentsen
93747f2766 switch base cover did not appear (#4454)
Thanks for testing the PR @emilhetty 👍
2016-11-19 12:33:08 +01:00
John Arild Berentsen
7af438fa2f Hound for zwave climate (#4465) 2016-11-19 10:19:22 +01:00
Fabian Affolter
2b5fcd737b PVOutput sensor (#4203)
* Add PVOutput sensor

* Remove attributes

* Revert `verify_ssl` back to true
2016-11-19 10:04:03 +01:00
John Arild Berentsen
2b320f23fc Hound comments (#4464) 2016-11-19 09:46:02 +01:00
John Arild Berentsen
679d500e61 Neato refactor and support for sensors (#4319)
* Imporvements to neato

* Review changes
2016-11-19 00:14:40 -08:00
mnestor
613615433a Google Calendar round 2 (#4161)
* Google Calendar round 2

* Add google back to .coveragerc

* Update __init__.py
2016-11-18 22:29:20 -08:00
Fabian Affolter
f70ff66d11 Upgrade batinfo to 0.4.2 (#4452) 2016-11-18 22:04:15 -08:00
Paulus Schoutsen
d2bbc6ef70 Upgrade linter (#4461) 2016-11-18 21:47:59 -08:00
Paulus Schoutsen
37e28428c1 Merge remote-tracking branch 'origin/master' into dev 2016-11-18 18:39:11 -08:00
Pascal Vizeli
c56f99baaf Async migration device_tracker (#4406)
* Async migration device_tracker

* change location stuff to async

* address paulus comments

* fix lint & add async discovery listener

* address paulus comments v2

* fix tests

* fix test_mqtt

* fix test_init

* fix gps_acc

* fix lint

* change async_update_stale to callback
2016-11-18 23:35:08 +01:00
Erik Eriksson
265232af98 only check heater status if present (#4459) 2016-11-18 14:12:51 -08:00
Fabian Affolter
e6c4113c5b Fix lint issues for 0.33 (#4451)
* Fix PEP257 issues

* Fix ident

* Fix lint issues

* Update docstrings

* Fix indent

* Fix indent

* Fix lint issues

* Fix lint issue

* Again lint
2016-11-18 23:05:03 +01:00
Igor Shults
c86e1b31b3 Fix typo in OWM (#4458) 2016-11-18 22:54:46 +01:00
Lewis Juggins
5912316496 pywebpush update to 0.6.1 (#4449) 2016-11-18 13:03:44 -08:00
John Arild Berentsen
58f0655298 ZWave Light: Use Configurable refresh (#4437)
* Use Configurable refresh

* Use super instead of object
2016-11-18 21:59:01 +01:00
John Arild Berentsen
43a93fb345 ZWave: Fix missing battery_level, node_id and location (#4422)
* Fix missing battery_level, node_id and location

* use super instead of object
2016-11-18 21:42:30 +01:00
Daniel Høyer Iversen
36b338051b Merge pull request #4450 from home-assistant/flux_led_lib_09
Upgrade flux led lib
2016-11-18 14:18:12 +01:00
Daniel Hoyer Iversen
fc566309c1 Upgrade flux led lib 2016-11-18 13:20:51 +01:00
Sean Dague
23ce9949b1 Merge pull request #4447 from sdague/proliphix
bump proliphix library version
2016-11-18 06:56:04 -05:00
Sean Dague
275c80183c bump proliphix library version
This fixes an upstream bug with daylight savings time handling
2016-11-18 05:05:05 -05:00
Sean Dague
cd1655f43b create light.hue_activate_scene service (#4425)
* create light.hue_activate_scene service

This creates a light.hue_activate_scene service that takes group_name
and scene_name, and calls phue's bridge.run_scene with those
parameters. This allows calling hue bridge stored scene names by name
during automation.

This only currently works reliably in 1 hue hub configurations (which
is most of them). Phue will be further enhanced to display warnings
when it can't figure out what to do with the parameters passed in to HA.

* Update hue.py
2016-11-17 22:14:06 -08:00
jnimmo
1a117d0bea Add keypress & output control services to Envisalink component (#3932)
* Add keypress & output control services to Envisalink component

Add services to allow sending custom keypresses and activating
programmable outputs on an alarm control panel.
Implemented for the Envisalink alarm, and moving to new version of
pyenvisalink to support this.

Replicated the service handler mapping code from Cover component into
Alarm Control Panel to allow handling alternative schemas if required
by new services.

* Update requirements_all.txt

* Updated services.yaml

* Removed requirement to enter code in HA UI

Incorporated changes suggested by @sriram
https://github.com/srirams/home-assistant/commit/2f8deb70cb5f3621a69b6b9
acb72f8e29123650c

Including pending state for exit/entry delay

Clarified services to use the code passed to them as a first priority,
otherwise use the code from configuration

Swapped back to using NotImplementedError for the service definitions

* - Add support for alarm_keypress to manual alarm (functions like a standard alarm keypad where entering the code disarms or arms the alarm)
- Add tests for alarm_keypress to manual alarm
- Style corrections (too many returns, comment & whitespace issues)

* Removed alarm_output_control service as unable to incorporate in the demo/test in a meaningful way

* Add keypress & output control services to Envisalink component

Add services to allow sending custom keypresses and activating
programmable outputs on an alarm control panel.
Implemented for the Envisalink alarm, and moving to new version of
pyenvisalink to support this.

Replicated the service handler mapping code from Cover component into
Alarm Control Panel to allow handling alternative schemas if required
by new services.

* Update requirements_all.txt

* Updated services.yaml

* Removed requirement to enter code in HA UI

Incorporated changes suggested by @sriram
https://github.com/srirams/home-assistant/commit/2f8deb70cb5f3621a69b6b9
acb72f8e29123650c

Including pending state for exit/entry delay

Clarified services to use the code passed to them as a first priority,
otherwise use the code from configuration

Swapped back to using NotImplementedError for the service definitions

* - Add support for alarm_keypress to manual alarm (functions like a standard alarm keypad where entering the code disarms or arms the alarm)
- Add tests for alarm_keypress to manual alarm
- Style corrections (too many returns, comment & whitespace issues)

* Removed alarm_output_control service as unable to incorporate in the demo/test in a meaningful way

* Moved the Alarm_Keypress service into Envisalink component out of the generic

* Update envisalink.py

* Update services.yaml
2016-11-17 22:13:22 -08:00
Fabian Affolter
944bb8474f Change validation to optional (#4400) 2016-11-17 22:09:57 -08:00
Magnus Ihse Bursie
779f520c56 Make UI more responsive to power off for Samsung Smart TV (#4438) 2016-11-17 22:00:18 -08:00
Magnus Ihse Bursie
82ed7b6b08 Fix so shell script adheres to posix standards. (#4439) 2016-11-17 21:59:53 -08:00
Paulus Schoutsen
af77341494 Add sensor to show how many clients are connected. (#4430)
* Add sensor to show how many clients are connected.

* Lint

* Fix tests
2016-11-17 21:54:47 -08:00
Paulus Schoutsen
23fb8c4cdd Convert script component to async (#4427) 2016-11-17 21:50:01 -08:00
Paulus Schoutsen
726bc5b670 Do not report on shutting down errors (#4431)
* Do not report on shutting down errors

* Lint
2016-11-17 12:02:43 -08:00
Open Home Automation
b615b3349f Fix for Miflora 2.6.6 firmware (#4436) 2016-11-17 08:40:21 -08:00
Paulus Schoutsen
0f59bb208c Migrate callbacks to use schedule_update_ha_state (#4426)
* Migrate callbacks to use schedule_update_ha_state

* Migrate MQTT sensor callback to async

* Migrate wemo to not update inside schedule_update_ha_state

* Make MQTT switch async

* Fix nx584 test

* Migrate tellstick callback

* Migrate vera callback

* Alarm control panel - manual: use async callbacks

* Run the switch rest tests that work
2016-11-17 07:34:46 -08:00
Paulus Schoutsen
38d201a54a Increase logging level of errors while doing jobs (#4429) 2016-11-16 23:01:14 -08:00
Pascal Vizeli
c8bc1e3c5d change add_job to use call_soon_threadsafe (#4410)
* change add_job to use call_soon_threadsafe

* address comments from paulus

* Tweak core tests

* Fix tests Python 3.4.2
2016-11-16 20:00:08 -08:00
Lewis Juggins
a862bc4edc Fix DLink async I/O (#4301) 2016-11-16 18:55:58 -08:00
Magnus Ihse Bursie
b0e3d5a576 Better handling of accented characters in slugify (#4399) (#4423)
* Better handling of accented characters in slugify (#4399)

* Update __init__.py
2016-11-16 15:05:10 -08:00
Paulus Schoutsen
f006b00dc1 Fix spelling schedule_update_ha_state (#4415) 2016-11-16 08:26:29 -08:00
bestlibre
1fff6ce438 Deduplicate MQTT_PUBLISH_SCHEMA definition (#4411) 2016-11-16 08:19:00 -08:00
Sean Dague
c06c82905a dynamically fetch yamaha media playback support (#4385)
This makes it so that media playback support for inputs is dynamically
fetched from the receiver, instead of assuming that all playback
commands work for all inputs.

Tests are added for this, using a FakeYamaha class, which has some
sample data stubbed in for key methods that need to be called. We also
include an example of the desc.xml needed to dynamically parse these
features for these tests (as this is done in platform init).
2016-11-15 21:56:40 -08:00
Paulus Schoutsen
2b86d89bb4 Fix tplink test 2016-11-15 21:26:03 -08:00
Sean Dague
7bdb79bd54 bump phue to 0.9 (#4404)
This increases the phue library to 0.9, which includes some basic
Scene support that could be consumed from home assistant.
2016-11-15 21:14:54 -08:00
Pascal Vizeli
41aaeb715a Convert switch to AsnycIO (#4382)
* Convert switch to AsnycIO

* Move update entity to service

* use time better for faster handling

* Change to suggestion from paulus

* Use new shedule_update_ha_state

* fix lint

* minimize executor calls
2016-11-15 21:06:50 -08:00
Fabian Affolter
5d8a465c18 Add timeout to requests (#4398) 2016-11-15 21:02:17 -08:00
John Arild Berentsen
c6f5a5443f Make zwave climate entities contain it's respective setpoints (#4357)
* Make zwave entities contain it's respective setpoints

* Add fan state
2016-11-15 13:14:29 +01:00
Fabian Affolter
d6cb102f63 Merge pull request #4144 from dasos/squeezebox_name_fix
Squeezebox name
2016-11-15 10:06:10 +01:00
Fabian Affolter
edde76e544 Fix validation and use consts (mqtt) (#4396) 2016-11-15 08:33:42 +01:00
Fabian Affolter
d5fff2f94a Fix validation and use consts (mqtt) 2016-11-15 08:21:44 +01:00
bestlibre
0e0ba28249 support for last will and birth message for mqtt (#4381) 2016-11-14 22:18:33 -08:00
Paulus Schoutsen
6745e83a6c Merge pull request #4394 from home-assistant/release-0-32-4
0.32.4
2016-11-14 22:04:43 -08:00
Paulus Schoutsen
44bc057fdb Version bump to 0.32.4 2016-11-14 21:34:40 -08:00
hexa-
96b8d8fcfa http: reimplement X-Forwarded-For parsing (#4355)
This feature needs to be enabled through the `http.use_x_forwarded_for` option,
satisfying security concerns of spoofed remote addresses in untrusted network
environments.

The testsuite was enhanced to explicitly test the functionality of the
header.

Fixes #4265.

Signed-off-by: Martin Weinelt <hexa@darmstadt.ccc.de>
2016-11-14 21:33:34 -08:00
Sean Dague
fc2df34206 Pin versions on linters for tests
The linters really need to specify an exact version, because when
either flake8 or pylint release a new version, a whole lot of new
issues are caught, causing failures on the code unrelated to the
patches being pushed.

Pinning is a best practice for linters. This allows patches which move
forward the linter version to happen with any code fixes required for
it to pass.
2016-11-14 21:32:02 -08:00
Paulus Schoutsen
09c29737de Fix device tracker sending invalid event data 2016-11-14 21:31:17 -08:00
Paulus Schoutsen
4c01b47945 device_tracker.see should not call async methods (#4377) 2016-11-14 21:31:06 -08:00
Paulus Schoutsen
7aaf3a46db Fix device tracker sending invalid event data (#4393) 2016-11-14 21:28:57 -08:00
Paulus Schoutsen
d774ba46c7 Fix device tracker sending invalid event data 2016-11-14 20:59:29 -08:00
Lewis Juggins
4c37ee8884 Handle live content better in Kodi (#4388) 2016-11-14 20:11:22 -08:00
Paulus Schoutsen
7f5f458074 Faster async entity update on component. (#4384) 2016-11-14 18:54:38 -08:00
Paulus Schoutsen
479457d6ec device_tracker.see should not call async methods (#4377) 2016-11-14 18:35:58 -08:00
Paulus Schoutsen
7e73d27dd1 Do not serve HTTP requests while stopping (#4378) 2016-11-14 18:33:53 -08:00
Martin Wood
e7ffec87ac Squeezebox name fix #4019 2016-11-14 21:46:05 +00:00
Robbie Trencheny
2d47b187c5 notify.html5: decode bytes values in registration data (#4379)
Occassionally the values of `keys` and `p256h` are bytes objects instead of
strings. As JSON by default does not serialize bytes objects let's decode
bytes objects to unicode strings.

Resolves the registration issue mentioned in #4012.

Signed-off-by: Martin Weinelt <hexa@darmstadt.ccc.de>
2016-11-14 11:35:31 -08:00
Sean Dague
fe2103dedb Merge pull request #4386 from sdague/lint
Pin versions on linters for tests
2016-11-14 13:58:50 -05:00
Sean Dague
7bf5d1c662 Pin versions on linters for tests
The linters really need to specify an exact version, because when
either flake8 or pylint release a new version, a whole lot of new
issues are caught, causing failures on the code unrelated to the
patches being pushed.

Pinning is a best practice for linters. This allows patches which move
forward the linter version to happen with any code fixes required for
it to pass.
2016-11-14 13:50:27 -05:00
pvizeli
cb24282040 Faster async entity update on component. 2016-11-14 14:18:04 +01:00
Fabian Affolter
bd9429d3af Upgrade sendgrid to 3.6.2 (#4370) 2016-11-14 09:35:08 +01:00
Martin Weinelt
d7a005ad0f notify.html5: decode bytes values in registration data
Occassionally the values of `keys` and `p256h` are bytes objects instead of
strings. As JSON by default does not serialize bytes objects let's decode
bytes objects to unicode strings.

Resolves the registration issue mentioned in #4012.

Signed-off-by: Martin Weinelt <hexa@darmstadt.ccc.de>
2016-11-14 05:14:18 +01:00
Paulus Schoutsen
2e2a996a8e Do not serve HTTP requests while stopping 2016-11-13 18:12:50 -08:00
Paulus Schoutsen
0364498dee Add .hound.yml 2016-11-13 11:34:09 -08:00
Daniel Høyer Iversen
c5fdd4392a Merge pull request #4373 from home-assistant/flux_led_color_bright
support color and brightness in flux_led light
2016-11-13 20:33:48 +01:00
Daniel Hoyer Iversen
895454b6c3 support color and brightness in flux_led light 2016-11-13 11:10:27 +01:00
Nathan Henrie
2109b7a1b9 Use entity_id for backend, friendly name for frontend (#4343)
* Use entity_id for backend, friendly name for frontend

Closes https://github.com/home-assistant/home-assistant/issues/3434

Command line switches had the option to set a `friendly_name` reportedly
for use in the front end. However, if set, it was also being used as the
`entity_id`.

This did not seem like obvious behavior to me. This PR changes the
behavior so the entity_id is the object_id, which must already be
unique, and is an obvious place to have a very predictable slug (even if
long or unsightly), and the friendly name (if set) is used for the
display.

Example:

```yaml
switch:
  platform: command_line
  switches:
    rf_kitchen_light_one:
      command_on: switch_command on kitchen
      command_off: switch_command off kitchen
      command_state: query_command kitchen
      value_template: '{{ value == "online" }}'
      friendly_name: "Beautiful bright kitchen light!"
```

If you were using in an automation or from dev tools, would use:
`switch.rf_kitchen_light_one`, but your front end would still show `Beautiful
bright kitchen light!`

* Add new arg to test_assumed_state_should_be_true_if_command_state_is_false

* Import ENTITY_ID _FORMAT from existing, rename device_name to object_id

* Rename `device_name` to `object_id`

* Test that `entity_id` and `name` are set as expected
2016-11-12 22:46:23 -08:00
Pascal Vizeli
71a305ea45 Hotfix deadlock on platform setup (#4354)
* Hotfix deadlock on platform setup

* fix wrong import
2016-11-12 16:19:13 -08:00
hexa-
e73634e6c7 http: reimplement X-Forwarded-For parsing (#4355)
This feature needs to be enabled through the `http.use_x_forwarded_for` option,
satisfying security concerns of spoofed remote addresses in untrusted network
environments.

The testsuite was enhanced to explicitly test the functionality of the
header.

Fixes #4265.

Signed-off-by: Martin Weinelt <hexa@darmstadt.ccc.de>
2016-11-12 16:14:39 -08:00
Pascal Vizeli
3d47ad5018 Use hass aiohttp connector for ssl connection (#4344) 2016-11-12 16:00:31 -08:00
Erik Eriksson
c823ea9f2a Don't fail if component name is None. Fixes (#4345)
https://github.com/home-assistant/home-assistant/issues/4326

Might fix https://github.com/home-assistant/home-assistant/issues/4326
2016-11-12 14:16:27 -08:00
Fabian Affolter
75bcb1ff0f Upgrade schiene to 0.18 (#4359) 2016-11-12 12:30:05 -08:00
Sean Dague
1663cc9084 Fix typo in generic thermostat (#4348)
It looks like a copy / paste error was made when doing the min/max
code. This fixes that.
2016-11-11 17:42:58 +01:00
Christian Brædstrup
17cfcc981d D-Link switch version bump of external library (#4351) 2016-11-11 17:38:12 +01:00
Hugo Dupras
60fabaec24 Add timeout for Netatmo binary sensor (#4280)
* Add time limit for Netatmo binary sensor

* Change limit to timeout

Signed-off-by: Hugo D. (jabesq) <jabesq@gmail.com>

* Update requirements_all.txt
2016-11-10 23:04:11 -08:00
Marcelo Moreira de Mello
5e44934e7e Added some extra options to Weather Underground component (#4306)
* Added some extra options to Weather Underground component

* Added Location and Elevation options

* Fixed if statement

* Fixed lint

* Updated tests including  elevation and location

* Update wunderground.py
2016-11-10 23:01:20 -08:00
Lewis Juggins
01a6c1c1c8 Add strptime template function (#3950) 2016-11-10 22:57:44 -08:00
Jeffrey Lin
cd1b0ac67d Added NVRAM-based MAC to IP mapping as backup to ARP tables (#4189) 2016-11-10 22:46:58 -08:00
Paulus Schoutsen
2bfded7153 MQTT.Server will use HASS eventloop (#3429) 2016-11-10 22:45:38 -08:00
Javier González Calleja
20af5cb5b4 Daily consumption information for HS110 (#4206)
* Add daily consumption information for HS110

* Fixing code review

* Fixing code review

* Fixing code review
2016-11-10 22:43:16 -08:00
Paulus Schoutsen
080f56e0f5 Merge pull request #4342 from home-assistant/release-0-32-3
0.32.3
2016-11-10 21:59:39 -08:00
Paulus Schoutsen
173e15e733 Version bump to 0.32.3 2016-11-10 21:50:05 -08:00
Paulus Schoutsen
72407c2f95 Make yr compatible with 0.32 2016-11-10 21:49:56 -08:00
Paulus Schoutsen
1b79722b69 Fix KNX async I/O (#4267) 2016-11-10 21:43:50 -08:00
Pascal Vizeli
cc5233103c Fix rest switch default template (#4331) 2016-11-10 21:43:50 -08:00
Daniel Høyer Iversen
2feea1d1eb Add support for rgb light in led flux, fixes issue #4303 (#4332) 2016-11-10 21:43:50 -08:00
Johann Kellerman
2c39c39d52 Improve async generic camera's error handling (#4316)
* Handle errors

* Feedback

* DisconnectedError
2016-11-10 21:43:48 -08:00
Paulus Schoutsen
6e6b1ef7ab fix panasonic viera doing I/O in event loop (#4341) 2016-11-10 21:42:41 -08:00
Pascal Vizeli
55ddaf1ee7 Synology SSL fix & Error handling (#4325)
* Synology SSL fix & Error handling

* change handling for cookies/ssl

* fix use not deprecated functions

* fix lint

* change verify

* fix connector close to coro

* fix force close

* not needed since websession close connector too

* fix params

* fix lint
2016-11-10 21:42:37 -08:00
Pascal Vizeli
6860d9b096 Update SoCo to 0.12 (#4337)
* Update SoCo to 0.12

* fix req
2016-11-10 21:41:28 -08:00
Sean Dague
3e1cc4282e Fix "argument of type 'NoneType' is not iterable" during discovery (#4279)
* Fix "argument of type 'NoneType' is not iterable" during discovery

When yamaha receivers are dynamically discovered, there config is
empty, which means that we need to set zone_ignore to [] otherwise the
iteration over receivers fails.

* Bump rxv library version to fix play_status bug

rxv version 0.3 will issue the play_status command even for sources
that don't support it, causing stack traces during updates when
receivers are on HDMI inputs.

This was fixed in rxv 0.3.1. Bump to fix bug #4226.

* Don't discovery receivers that we've already configured

The discovery component doesn't know anything about already configured
receivers. This means that specifying a receiver manually will make it
show up twice if you have the discovery component enabled.

This puts a platform specific work around here that ensures that if
the media_player is found, we ignore the discovery system.
2016-11-10 21:41:28 -08:00
Jan Losinski
200bdb30ff Change pilight systemcode validation to integer (#4286)
* Change pilight systemcode validation to integer

According to the pilight code the systemcode should be an integer and
not a string (it is an int in the pilight code). Passing this as a
string caused errors from pilight:
"ERROR: elro_800_switch: insufficient number of arguments"

This fixes #4282

* Change pilight unit-id to positive integer

According to the pilight code the unit of an entity is also evrywhere
handled as an integer. So converting and passing this as string causes
pilight not to work.

This fixes #4282

Signed-off-by: Jan Losinski <losinski@wh2.tu-dresden.de>
2016-11-10 21:41:28 -08:00
Paulus Schoutsen
eb17ba970c Increase update delay (#4321) 2016-11-10 21:41:28 -08:00
Paulus Schoutsen
ffe4c425af Fix Tellstick doing I/O inside event loop (#4268) 2016-11-10 21:41:28 -08:00
Jesse Newland
a18fdbfbb8 Fix alarm.com I/O inside properties (#4307)
* Fix alarm.com I/O inside properties

* First line should end with a period

* Not needed

* Fetch state on init
2016-11-10 21:41:28 -08:00
Lewis Juggins
58600f25b3 Fix OWM async I/O (#4298) 2016-11-10 21:41:28 -08:00
Pascal Vizeli
749fc583ea Fix rest switch default template (#4331) 2016-11-10 21:32:08 -08:00
Daniel Høyer Iversen
b07d887d77 Add support for rgb light in led flux, fixes issue #4303 (#4332) 2016-11-10 21:30:52 -08:00
Johann Kellerman
9bb94a4512 Improve async generic camera's error handling (#4316)
* Handle errors

* Feedback

* DisconnectedError
2016-11-10 21:28:22 -08:00
Paulus Schoutsen
e76d553513 fix panasonic viera doing I/O in event loop (#4341) 2016-11-10 21:17:44 -08:00
Pascal Vizeli
844799a1f7 Synology SSL fix & Error handling (#4325)
* Synology SSL fix & Error handling

* change handling for cookies/ssl

* fix use not deprecated functions

* fix lint

* change verify

* fix connector close to coro

* fix force close

* not needed since websession close connector too

* fix params

* fix lint
2016-11-10 21:04:47 -08:00
Pascal Vizeli
e005ebe989 Update SoCo to 0.12 (#4337)
* Update SoCo to 0.12

* fix req
2016-11-10 21:01:42 -08:00
Sean Dague
e9d19c1dcc Fix "argument of type 'NoneType' is not iterable" during discovery (#4279)
* Fix "argument of type 'NoneType' is not iterable" during discovery

When yamaha receivers are dynamically discovered, there config is
empty, which means that we need to set zone_ignore to [] otherwise the
iteration over receivers fails.

* Bump rxv library version to fix play_status bug

rxv version 0.3 will issue the play_status command even for sources
that don't support it, causing stack traces during updates when
receivers are on HDMI inputs.

This was fixed in rxv 0.3.1. Bump to fix bug #4226.

* Don't discovery receivers that we've already configured

The discovery component doesn't know anything about already configured
receivers. This means that specifying a receiver manually will make it
show up twice if you have the discovery component enabled.

This puts a platform specific work around here that ensures that if
the media_player is found, we ignore the discovery system.
2016-11-10 20:44:38 -08:00
Jan Losinski
7d2ab4fce6 Change pilight systemcode validation to integer (#4286)
* Change pilight systemcode validation to integer

According to the pilight code the systemcode should be an integer and
not a string (it is an int in the pilight code). Passing this as a
string caused errors from pilight:
"ERROR: elro_800_switch: insufficient number of arguments"

This fixes #4282

* Change pilight unit-id to positive integer

According to the pilight code the unit of an entity is also evrywhere
handled as an integer. So converting and passing this as string causes
pilight not to work.

This fixes #4282

Signed-off-by: Jan Losinski <losinski@wh2.tu-dresden.de>
2016-11-10 13:14:40 -08:00
Pascal Vizeli
ba2ea35089 Add logging to platform/component setup (#4300)
* Add timeout to platform/component

* Revert "Add timeout to platform/component"

This reverts commit 280a311e48.

* Add logging data

* Change log message with paulus comments
2016-11-10 18:46:31 +01:00
Erik Eriksson
ade62faa38 Don't fail if component name is None. Fixes (#4334)
https://github.com/home-assistant/home-assistant/issues/4326

Might fix https://github.com/home-assistant/home-assistant/issues/4326
2016-11-10 08:46:32 -08:00
Finbarr Brady
ee322dbbdc Cisco IOS device tracker support (#4193) 2016-11-09 22:36:57 +02:00
John Arild Berentsen
0d4141bf13 Add missing Index labels (#4328) 2016-11-09 19:11:24 +01:00
Christopher Viel
d404ac8978 Add support for off script to WOL switch (#4258) 2016-11-09 08:44:30 -08:00
Pascal Vizeli
71da21dcc8 Change pending task sheduler to time based cleanup (#4324)
* Change pending task sheduler to time based cleanup

* update unittest
2016-11-09 08:41:17 -08:00
Paulus Schoutsen
04dbc992ec Increase update delay (#4321) 2016-11-09 07:21:58 -08:00
Paulus Schoutsen
6d0e08cf7d Fix KNX async I/O (#4267) 2016-11-08 21:00:33 -08:00
Paulus Schoutsen
1e0025acae Fix Tellstick doing I/O inside event loop (#4268) 2016-11-08 20:25:19 -08:00
Pascal Vizeli
8fc853ba11 Add more unittest for async_add_job (#4320)
* Add more unittest for async_add_job

* fix test

* lint
2016-11-08 20:01:05 -08:00
sustah
8cbb8f6527 Update dlink.py (#4317)
corrected "total consumption" units from W to kWh
2016-11-08 19:58:27 -08:00
Jesse Newland
4f86c9ecda Fix alarm.com I/O inside properties (#4307)
* Fix alarm.com I/O inside properties

* First line should end with a period

* Not needed

* Fetch state on init
2016-11-08 19:57:46 -08:00
Lewis Juggins
9561fed650 Fix Dark Sky async I/O (#4299) 2016-11-08 19:46:44 -08:00
Lewis Juggins
67b599475e Fix OWM async I/O (#4298) 2016-11-08 18:57:56 -08:00
Pascal Vizeli
114ece1848 Fix possible sigterm / unittest / Fix all lazy test (#4297)
* replace weakref with a list

* add unittest

* fix lint

* fix handling

* fix unittest

* change code style

* fix lazy tests
2016-11-08 10:24:50 +01:00
Fabian Affolter
c05815cced Upgrade sqlalchemy to 1.1.3 (#4277) 2016-11-07 23:08:17 -08:00
Pascal Vizeli
2e0c185740 Async cleanup part 3 (#4302) 2016-11-07 22:31:40 -08:00
Johann Kellerman
231ef40f53 iOS links (#4295) 2016-11-08 00:07:24 +01:00
Fabian Affolter
b4159c7dc9 Upgrade python-digitalocean to 1.10.1 (#4276) 2016-11-06 23:49:25 -08:00
Fabian Affolter
8cc5fc1369 Upgrade psutil to 5.0.0 (#4275) 2016-11-06 23:49:11 -08:00
Paulus Schoutsen
fc3235fb6d Merge pull request #4271 from home-assistant/release-0-32-2
Release 0 32 2
2016-11-06 23:40:06 -08:00
David-Leon Pohl
d129df93dd Hotfix #4272 (#4273) 2016-11-06 23:34:45 -08:00
David-Leon Pohl
67336a111b Hotfix #4272 (#4273) 2016-11-06 23:34:32 -08:00
Paulus Schoutsen
0af1a96f14 Lint 2016-11-06 23:24:25 -08:00
andyat
272899ec96 Fix setting temperature in Celsius on radiotherm CT50 (#4270) 2016-11-06 23:21:08 -08:00
andyat
7d28d9d6b4 Fix setting temperature in Celsius on radiotherm CT50 (#4270) 2016-11-06 23:18:06 -08:00
Paulus Schoutsen
6a92e27e2f Version bump to 0.32.2 2016-11-06 23:12:37 -08:00
Paulus Schoutsen
faceb4c1dc Sequential updates for non-async entities 2016-11-06 23:12:20 -08:00
Paulus Schoutsen
6d5f00098a Move Honeywell I/O out of event loop (#4244) 2016-11-06 23:09:31 -08:00
Pascal Vizeli
618a86a37c Set executor to 15 and help to reduce flooting async core with updates (#4252)
* Set executor to 15 and help to reduce flooting async core with udpates

* fix typing

* if it a executor, wait

* address comments from paulus

* add space for style :)

* fix spell

* Update entity_component.py

* Update entity_component.py
2016-11-06 22:28:03 -08:00
Paulus Schoutsen
880ef8af48 Remove broken disable verify ssl synology (#4269) 2016-11-06 22:17:56 -08:00
William Scanlon
95124c7ddb Revert "Catch AttributeError on Wink PubNub update" (#4263) 2016-11-06 20:05:42 -08:00
William Scanlon
0aba227300 Catch AttributeError (#4253) 2016-11-06 16:04:57 -08:00
Martin Hjelmare
734bd75fd3 Fix mysensors overwriting gateway in GATEWAYS (#4013)
GATEWAYS was a dict, so would overwrite item if key was the same. This
would happen when using multiple MQTT gateways, since the device id is
the same (`mqtt`).

* Fix by changing GATEWAYS from dict into list.
* Use hass data to store mysensors gateways instead of having GATEWAYS
  be a global.
2016-11-06 10:49:43 -08:00
Frantz
0c5e077091 Updated netdisco to 0.7.6 (#4250) 2016-11-06 10:43:13 -08:00
Nicolas Graziano
1ed2f8ae91 Update braviarc to 0.3.6 (#4246)
Add HDMI sources.
Sources ordered.
2016-11-06 09:27:55 -08:00
Paulus Schoutsen
a343c20404 Async gather wait (#4247)
* Fix config validation for input_*, script

* Allow scheduling coroutines

* Validate entity ids when entity ids set by platform

* Async: gather -> wait

* Script/Group: use async_add_job instead of create_task
2016-11-06 09:26:40 -08:00
Antoine Bertin
d4e8b831a0 Add mqtt_template light component (#4233)
* Add mqtt_template component

* Docstring copy paste party on overriden methods

* pep8 E501 🌟

* Add missing docstrings on unittests
2016-11-06 09:09:01 -08:00
Paulus Schoutsen
98f41d6b84 Tweak block_till_done (#4245) 2016-11-06 08:43:32 -08:00
Paulus Schoutsen
7774a03a55 Move Honeywell I/O out of event loop (#4244) 2016-11-06 07:53:54 -08:00
Fabian Affolter
c35e5c9997 Upgrade astral to 1.3 (#4238) 2016-11-06 07:36:16 -08:00
Fabian Affolter
5d862e426e Upgrade fuzzywuzzy to 0.14.0 (#4240) 2016-11-06 07:36:03 -08:00
William Scanlon
bab8d574fe Wink Thermostat support and NoneType error fixes (#4175) 2016-11-06 07:27:15 -08:00
Fabian Affolter
c980d26aae Upgrade distro to 1.0.1 (#4239) 2016-11-06 14:00:41 +02:00
Paulus Schoutsen
08f75f7935 Merge pull request #4235 from home-assistant/release-0-32-1
0.32.1
2016-11-05 17:09:10 -07:00
Brent Hughes
1ad14b8227 Updated Emulated_Hue to send request info as variables to scripts (#4010)
* Updated Emulated_Hue to send request info as variables to scripts

* Updated tests to not use the old mqtt

* Updated test to actualy use and validate the script variables

* Fixed the removal of time in a recent merge

* fixed test to not use a timer
2016-11-05 17:08:54 -07:00
Pascal Vizeli
382ac5c3b5 Async cleanups with new handling and executor (#4234) 2016-11-06 01:01:03 +01:00
Paulus Schoutsen
af297aa0dc Version bump to 0.32.1 2016-11-05 17:00:06 -07:00
Paulus Schoutsen
20e1b3eae0 Fix radiotherm I/O inside properties (#4227) 2016-11-05 16:59:52 -07:00
Paulus Schoutsen
28861221ae Remove chunked encoding (#4230) 2016-11-05 16:59:52 -07:00
Pascal Vizeli
f367c49fb9 Sonos fix for slow update (#4232)
* Sonos fix for slow update

* fix auto update on discovery

* fix unittest
2016-11-05 16:59:52 -07:00
Pascal Vizeli
ad8645baf4 Sonos fix for slow update (#4232)
* Sonos fix for slow update

* fix auto update on discovery

* fix unittest
2016-11-05 16:58:29 -07:00
Paulus Schoutsen
62785c2431 More async tests (#4223)
* Annotate test callbacks to be async

* Convert device_sun_light_trigger to be async
2016-11-05 16:36:20 -07:00
Paulus Schoutsen
22c3d014aa Remove chunked encoding (#4230) 2016-11-05 15:29:22 -07:00
Paulus Schoutsen
3f3127a290 Fix radiotherm I/O inside properties (#4227) 2016-11-05 13:28:11 -07:00
Danijel Stojnic
88fc64c8a0 Add Map support for Locative component (#4174)
* Add Map support for Locative component

The Locative App on the mobile is sending an HTTP request to the
server where also the GPS location is sent.
But the GPS location was not passed to the event device_tracker.see.

Use the passed GPS location from Locative and pass it to the
device_tracker.see event.

With this the device is then also shown on the HA Map component.

* Use existing constants for latitude and longitude

Use the existing constants from homeassistant.consts:

ATTR_LATITUDE for 'latitude'
ATTR_LONGITUDE for 'longitude'

* Reuse the "yield from self.hass.loop.run_in_executor" again

* Use variable gps_location
2016-11-05 13:05:15 -07:00
Samuel Bétrisey
1463fc4fe0 Add Swisscom Internet-Box device tracker (#4123)
* Add Swisscom Internet-Box device tracker

* Add Swisscom device tracker to .coveragerc

* Add timeout to requests
Fix formatting and add missing comments to pass the lint test

* Remove authentication which was not required

I realised that there was no need to be authenticated to get the
connected devices. Thanks Swisscom :/

* Moving config to a PLATFORM_SCHEMA and using voluptuous
2016-11-05 13:04:44 -07:00
Pascal Vizeli
ece58ce78f Remove ThreadPool with async executor (#4154)
* Remove ThreadPool with async executor

* Fix zigbee

* update unittest

* fix remote api

* add pending task to remote

* fix lint

* remove unused import

* remove old stuff for lazy tests

* fix bug and add a exception handler to executor

* change executor handling

* change to wait from gather

* fix unittest
2016-11-05 09:27:55 -07:00
Paulus Schoutsen
b67f1fed52 Version bump to 0.33.0.dev0 2016-11-05 08:53:13 -07:00
221 changed files with 8685 additions and 1549 deletions

View File

@@ -28,6 +28,9 @@ omit =
homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py
homeassistant/components/google.py
homeassistant/components/*/google.py
homeassistant/components/insteon_hub.py
homeassistant/components/*/insteon_hub.py
@@ -98,6 +101,9 @@ omit =
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/neato.py
homeassistant/components/*/neato.py
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
@@ -132,7 +138,7 @@ omit =
homeassistant/components/climate/knx.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py
@@ -144,12 +150,14 @@ omit =
homeassistant/components/device_tracker/bluetooth_le_tracker.py
homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py
@@ -278,6 +286,7 @@ omit =
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/pi_hole.py
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/serial_pm.py
@@ -306,7 +315,6 @@ omit =
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/mystrom.py
homeassistant/components/switch/neato.py
homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pilight.py

2
.hound.yml Normal file
View File

@@ -0,0 +1,2 @@
python:
enabled: true

View File

@@ -4,7 +4,7 @@ import logging
import logging.handlers
import os
import sys
from collections import defaultdict
from collections import OrderedDict
from types import ModuleType
from typing import Any, Optional, Dict
@@ -57,7 +57,7 @@ def async_setup_component(hass: core.HomeAssistant, domain: str,
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
if config is None:
config = defaultdict(dict)
config = {}
components = loader.load_order_component(domain)
@@ -142,6 +142,7 @@ def _async_setup_component(hass: core.HomeAssistant,
async_comp = hasattr(component, 'async_setup')
try:
_LOGGER.info("Setting up %s", domain)
if async_comp:
result = yield from component.async_setup(hass, config)
else:
@@ -165,15 +166,6 @@ def _async_setup_component(hass: core.HomeAssistant,
hass.config.components.append(component.DOMAIN)
# Assumption: if a component does not depend on groups
# it communicates with devices
if (not async_comp and
'group' not in getattr(component, 'DEPENDENCIES', [])):
if hass.pool is None:
hass.async_init_pool()
if hass.pool.worker_count <= 10:
hass.pool.add_worker()
hass.bus.async_fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
)
@@ -353,7 +345,7 @@ def from_config_dict(config: Dict[str, Any],
# run task
future = asyncio.Future(loop=hass.loop)
hass.loop.create_task(_async_init_from_config_dict(future))
hass.async_add_job(_async_init_from_config_dict(future))
hass.loop.run_until_complete(future)
return future.result()
@@ -373,6 +365,12 @@ def async_from_config_dict(config: Dict[str, Any],
Dynamically loads required components and its dependencies.
This method is a coroutine.
"""
setup_lock = hass.data.get('setup_lock')
if setup_lock is None:
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
yield from setup_lock.acquire()
core_config = config.get(core.DOMAIN, {})
try:
@@ -396,10 +394,12 @@ def async_from_config_dict(config: Dict[str, Any],
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
# Make a copy because we are mutating it.
# Convert it to defaultdict so components can always have config dict
# Use OrderedDict in case original one was one.
# Convert values to dictionaries if they are None
config = defaultdict(
dict, {key: value or {} for key, value in config.items()})
new_config = OrderedDict()
for key, value in config.items():
new_config[key] = value or {}
config = new_config
# Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys()
@@ -425,6 +425,8 @@ def async_from_config_dict(config: Dict[str, Any],
for domain in loader.load_order_components(components):
yield from _async_setup_component(hass, domain, config)
setup_lock.release()
return hass

View File

@@ -119,7 +119,7 @@ def async_setup(hass, config):
tasks.append(hass.services.async_call(
domain, service.service, data, blocking))
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)

View File

@@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
add_devices([AlarmDotCom(hass, name, code, username, password)])
add_devices([AlarmDotCom(hass, name, code, username, password)], True)
class AlarmDotCom(alarm.AlarmControlPanel):
@@ -54,12 +54,17 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._code = str(code) if code else None
self._username = username
self._password = password
self._state = STATE_UNKNOWN
@property
def should_poll(self):
"""No polling needed."""
return True
def update(self):
"""Fetch the latest state."""
self._state = self._alarm.state
@property
def name(self):
"""Return the name of the alarm."""
@@ -73,11 +78,11 @@ class AlarmDotCom(alarm.AlarmControlPanel):
@property
def state(self):
"""Return the state of the device."""
if self._alarm.state == 'Disarmed':
if self._state == 'Disarmed':
return STATE_ALARM_DISARMED
elif self._alarm.state == 'Armed Stay':
elif self._state == 'Armed Stay':
return STATE_ALARM_ARMED_HOME
elif self._alarm.state == 'Armed Away':
elif self._state == 'Armed Away':
return STATE_ALARM_ARMED_AWAY
else:
return STATE_UNKNOWN

View File

@@ -4,20 +4,45 @@ Support for Envisalink-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.envisalink/
"""
from os import path
import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.components.envisalink import (
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['envisalink']
DEVICES = []
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
ATTR_KEYPRESS = 'keypress'
ALARM_KEYPRESS_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_KEYPRESS): cv.string
})
def alarm_keypress_handler(service):
"""Map services to methods on Alarm."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
keypress = service.data.get(ATTR_KEYPRESS)
_target_devices = [device for device in DEVICES
if device.entity_id in entity_ids]
for device in _target_devices:
EnvisalinkAlarm.alarm_keypress(device, keypress)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -35,8 +60,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_panic_type,
EVL_CONTROLLER.alarm_state['partition'][part_num],
EVL_CONTROLLER)
add_devices([_device])
DEVICES.append(_device)
add_devices(DEVICES)
# Register Envisalink specific services
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(alarm.DOMAIN, SERVICE_ALARM_KEYPRESS,
alarm_keypress_handler,
descriptions.get(SERVICE_ALARM_KEYPRESS),
schema=ALARM_KEYPRESS_SCHEMA)
return True
@@ -66,42 +101,64 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
@property
def code_format(self):
"""The characters if code is defined."""
return self._code
"""Regex for code format or None if no code is required."""
if self._code:
return None
else:
return '^\\d{4,6}$'
@property
def state(self):
"""Return the state of the device."""
state = STATE_UNKNOWN
if self._info['status']['alarm']:
return STATE_ALARM_TRIGGERED
state = STATE_ALARM_TRIGGERED
elif self._info['status']['armed_away']:
return STATE_ALARM_ARMED_AWAY
state = STATE_ALARM_ARMED_AWAY
elif self._info['status']['armed_stay']:
return STATE_ALARM_ARMED_HOME
state = STATE_ALARM_ARMED_HOME
elif self._info['status']['exit_delay']:
state = STATE_ALARM_PENDING
elif self._info['status']['entry_delay']:
state = STATE_ALARM_PENDING
elif self._info['status']['alpha']:
return STATE_ALARM_DISARMED
else:
return STATE_UNKNOWN
state = STATE_ALARM_DISARMED
return state
def alarm_disarm(self, code=None):
"""Send disarm command."""
if self._code:
EVL_CONTROLLER.disarm_partition(
str(code), self._partition_number)
if code:
EVL_CONTROLLER.disarm_partition(str(code),
self._partition_number)
else:
EVL_CONTROLLER.disarm_partition(str(self._code),
self._partition_number)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if self._code:
EVL_CONTROLLER.arm_stay_partition(
str(code), self._partition_number)
if code:
EVL_CONTROLLER.arm_stay_partition(str(code),
self._partition_number)
else:
EVL_CONTROLLER.arm_stay_partition(str(self._code),
self._partition_number)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if self._code:
EVL_CONTROLLER.arm_away_partition(
str(code), self._partition_number)
if code:
EVL_CONTROLLER.arm_away_partition(str(code),
self._partition_number)
else:
EVL_CONTROLLER.arm_away_partition(str(self._code),
self._partition_number)
def alarm_trigger(self, code=None):
"""Alarm trigger command. Will be used to trigger a panic alarm."""
if self._code:
EVL_CONTROLLER.panic_alarm(self._panic_type)
EVL_CONTROLLER.panic_alarm(self._panic_type)
def alarm_keypress(self, keypress=None):
"""Send custom keypress."""
if keypress:
EVL_CONTROLLER.keypresses_to_partition(self._partition_number,
keypress)

View File

@@ -129,7 +129,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
if self._pending_time:
track_point_in_time(
self._hass, self.update_ha_state,
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time)
def alarm_arm_away(self, code=None):
@@ -143,7 +143,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
if self._pending_time:
track_point_in_time(
self._hass, self.update_ha_state,
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time)
def alarm_trigger(self, code=None):
@@ -155,11 +155,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
if self._trigger_time:
track_point_in_time(
self._hass, self.update_ha_state,
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time)
track_point_in_time(
self._hass, self.update_ha_state,
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time + self._trigger_time)
def _validate_code(self, code, state):

View File

@@ -41,3 +41,14 @@ alarm_trigger:
code:
description: An optional code to trigger the alarm control panel with
example: 1234
envisalink_alarm_keypress:
description: Send custom keypresses to the alarm
fields:
entity_id:
description: Name of the alarm control panel to trigger
example: 'alarm_control_panel.downstairs'
keypress:
description: 'String to send to the alarm panel (1-6 characters)'
example: '*71'

View File

@@ -66,6 +66,7 @@ def _platform_validator(config):
return getattr(platform, 'TRIGGER_SCHEMA')(config)
_TRIGGER_SCHEMA = vol.All(
cv.ensure_list,
[
@@ -165,7 +166,7 @@ def async_setup(hass, config):
for entity in component.async_extract_from_service(service_call):
tasks.append(entity.async_trigger(
service_call.data.get(ATTR_VARIABLES), True))
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def turn_onoff_service_handler(service_call):
@@ -174,7 +175,7 @@ def async_setup(hass, config):
method = 'async_{}'.format(service_call.service)
for entity in component.async_extract_from_service(service_call):
tasks.append(getattr(entity, method)())
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def toggle_service_handler(service_call):
@@ -185,7 +186,7 @@ def async_setup(hass, config):
tasks.append(entity.async_turn_off())
else:
tasks.append(entity.async_turn_on())
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def reload_service_handler(service_call):
@@ -348,8 +349,10 @@ def _async_process_config(hass, config, component):
tasks.append(entity.async_enable())
entities.append(entity)
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from component.async_add_entities(entities)
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
if entities:
yield from component.async_add_entities(entities)
return len(entities) > 0

View File

@@ -138,7 +138,7 @@ class FFmpegBinarySensor(BinarySensorDevice):
def _callback(self, state):
"""HA-FFmpeg callback for noise detection."""
self._state = state
self.update_ha_state()
self.schedule_update_ha_state()
def _start_ffmpeg(self, config):
"""Start a FFmpeg instance."""

View File

@@ -8,6 +8,7 @@ import logging
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt
from homeassistant.components.binary_sensor import (
BinarySensorDevice, SENSOR_CLASSES)
@@ -66,17 +67,18 @@ class MqttBinarySensor(BinarySensorDevice):
self._payload_off = payload_off
self._qos = qos
@callback
def message_received(topic, payload, qos):
"""A new MQTT message has been received."""
if value_template is not None:
payload = value_template.render_with_possible_json_value(
payload = value_template.async_render_with_possible_json_value(
payload)
if payload == self._payload_on:
self._state = True
self.update_ha_state()
hass.async_add_job(self.async_update_ha_state())
elif payload == self._payload_off:
self._state = False
self.update_ha_state()
hass.async_add_job(self.async_update_ha_state())
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)

View File

@@ -22,7 +22,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None:
return
for gateway in mysensors.GATEWAYS.values():
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
if not gateways:
return
for gateway in gateways:
# Define the S_TYPES and V_TYPES that the platform should handle as
# states. Map them in a dict of lists.
pres = gateway.const.Presentation

View File

@@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.netatmo import WelcomeData
from homeassistant.loader import get_component
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ["netatmo"]
@@ -33,6 +33,7 @@ CONF_CAMERAS = 'cameras'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME): cv.string,
vol.Optional(CONF_TIMEOUT): cv.positive_int,
vol.Optional(CONF_CAMERAS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
@@ -45,6 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup access to Netatmo binary sensor."""
netatmo = get_component('netatmo')
home = config.get(CONF_HOME, None)
timeout = config.get(CONF_TIMEOUT, 15)
import lnetatmo
try:
@@ -62,18 +64,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
camera_name not in config[CONF_CAMERAS]:
continue
for variable in sensors:
add_devices([WelcomeBinarySensor(data, camera_name, home,
add_devices([WelcomeBinarySensor(data, camera_name, home, timeout,
variable)])
class WelcomeBinarySensor(BinarySensorDevice):
"""Represent a single binary sensor in a Netatmo Welcome device."""
def __init__(self, data, camera_name, home, sensor):
def __init__(self, data, camera_name, home, timeout, sensor):
"""Setup for access to the Netatmo camera events."""
self._data = data
self._camera_name = camera_name
self._home = home
self._timeout = timeout
if home:
self._name = home + ' / ' + camera_name
else:
@@ -114,14 +117,17 @@ class WelcomeBinarySensor(BinarySensorDevice):
if self._sensor_name == "Someone known":
self._state =\
self._data.welcomedata.someoneKnownSeen(self._home,
self._camera_name)
self._camera_name,
self._timeout*60)
elif self._sensor_name == "Someone unknown":
self._state =\
self._data.welcomedata.someoneUnknownSeen(self._home,
self._camera_name)
self._camera_name,
self._timeout*60)
elif self._sensor_name == "Motion":
self._state =\
self._data.welcomedata.motionDetected(self._home,
self._camera_name)
self._camera_name,
self._timeout*60)
else:
return None

View File

@@ -123,7 +123,7 @@ class NX584Watcher(threading.Thread):
if not zone_sensor:
return
zone_sensor._zone['state'] = event['zone_state']
zone_sensor.update_ha_state()
zone_sensor.schedule_update_ha_state()
def _process_events(self, events):
for event in events:

View File

@@ -72,7 +72,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
def read_gpio(port):
"""Read state from GPIO."""
self._state = rpi_gpio.read_input(self._port)
self.update_ha_state()
self.schedule_update_ha_state()
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)

View File

@@ -63,7 +63,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
_LOGGER.error('No sensors added')
return False
hass.loop.create_task(async_add_devices(sensors, True))
yield from async_add_devices(sensors, True)
return True
@@ -84,7 +84,7 @@ class BinarySensorTemplate(BinarySensorDevice):
@callback
def template_bsensor_state_listener(entity, old_state, new_state):
"""Called when the target device changes state."""
hass.loop.create_task(self.async_update_ha_state(True))
hass.async_add_job(self.async_update_ha_state, True)
async_track_state_change(
hass, entity_ids, template_bsensor_state_listener)

View File

@@ -4,8 +4,11 @@ A sensor that monitors trands in other components.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.trend/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
@@ -87,13 +90,12 @@ class SensorTrend(BinarySensorDevice):
self.from_state = None
self.to_state = None
self.update()
@callback
def trend_sensor_state_listener(entity, old_state, new_state):
"""Called when the target device changes state."""
self.from_state = old_state
self.to_state = new_state
self.update_ha_state(True)
hass.async_add_job(self.async_update_ha_state(True))
track_state_change(hass, target_entity,
trend_sensor_state_listener)
@@ -118,7 +120,8 @@ class SensorTrend(BinarySensorDevice):
"""No polling needed."""
return False
def update(self):
@asyncio.coroutine
def async_update(self):
"""Get the latest data and update the states."""
if self.from_state is None or self.to_state is None:
return

View File

@@ -45,10 +45,10 @@ class WemoBinarySensor(BinarySensorDevice):
_LOGGER.info(
'Subscription update for %s',
_device)
self.update()
if not hasattr(self, 'hass'):
self.update()
return
self.update_ha_state(True)
self.schedule_update_ha_state()
@property
def should_poll(self):

View File

@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
at https://home-assistant.io/components/binary_sensor.wink/
"""
import json
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.sensor.wink import WinkDevice
@@ -53,12 +54,17 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
self.capability = self.wink.capability()
def _pubnub_update(self, message, channel):
if 'data' in message:
json_data = json.dumps(message.get('data'))
else:
json_data = message
self.wink.pubnub_update(json.loads(json_data))
self.update_ha_state()
try:
if 'data' in message:
json_data = json.dumps(message.get('data'))
else:
json_data = message
self.wink.pubnub_update(json.loads(json_data))
self.update_ha_state()
except (AttributeError, KeyError):
error = "Pubnub returned invalid json for " + self.name
logging.getLogger(__name__).error(error)
self.update_ha_state(True)
@property
def is_on(self):

View File

@@ -96,7 +96,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_ha_state()
self.schedule_update_ha_state()
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
@@ -112,19 +112,19 @@ class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
# If it's active make sure that we set the timeout tracker
if sensor_value.data:
track_point_in_time(
self._hass, self.update_ha_state,
self._hass, self.async_update_ha_state,
self.invalidate_after)
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
self.update_ha_state()
self.schedule_update_ha_state()
if value.data:
# only allow this value to be true for re_arm secs
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
seconds=self.re_arm_sec)
track_point_in_time(
self._hass, self.update_ha_state,
self._hass, self.async_update_ha_state,
self.invalidate_after)
@property

View File

@@ -0,0 +1,183 @@
"""
Support for Google Calendar event device sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/calendar/
"""
import logging
import re
from homeassistant.components.google import (CONF_OFFSET,
CONF_DEVICE_ID,
CONF_NAME)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers.config_validation import time_period_str
from homeassistant.helpers.entity import Entity, generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.util import dt
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'calendar'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
def setup(hass, config):
"""Track states and offer events for calendars."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN)
component.setup(config)
return True
DEFAULT_CONF_TRACK_NEW = True
DEFAULT_CONF_OFFSET = '!!'
# pylint: disable=too-many-instance-attributes
class CalendarEventDevice(Entity):
"""A calendar event device."""
# Classes overloading this must set data to an object
# with an update() method
data = None
# pylint: disable=too-many-arguments
def __init__(self, hass, data):
"""Create the Calendar Event Device."""
self._name = data.get(CONF_NAME)
self.dev_id = data.get(CONF_DEVICE_ID)
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT,
self.dev_id,
hass=hass)
self._cal_data = {
'all_day': False,
'offset_time': dt.dt.timedelta(),
'message': '',
'start': None,
'end': None,
'location': '',
'description': '',
}
self.update()
def offset_reached(self):
"""Have we reached the offset time specified in the event title."""
if self._cal_data['start'] is None or \
self._cal_data['offset_time'] == dt.dt.timedelta():
return False
return self._cal_data['start'] + self._cal_data['offset_time'] <= \
dt.now(self._cal_data['start'].tzinfo)
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def device_state_attributes(self):
"""State Attributes for HA."""
start = self._cal_data.get('start', None)
end = self._cal_data.get('end', None)
start = start.strftime(DATE_STR_FORMAT) if start is not None else None
end = end.strftime(DATE_STR_FORMAT) if end is not None else None
return {
'message': self._cal_data.get('message', ''),
'all_day': self._cal_data.get('all_day', False),
'offset_reached': self.offset_reached(),
'start_time': start,
'end_time': end,
'location': self._cal_data.get('location', None),
'description': self._cal_data.get('description', None),
}
@property
def state(self):
"""Return the state of the calendar event."""
start = self._cal_data.get('start', None)
end = self._cal_data.get('end', None)
if start is None or end is None:
return STATE_OFF
now = dt.now()
if start <= now and end > now:
return STATE_ON
if now >= end:
self.cleanup()
return STATE_OFF
def cleanup(self):
"""Cleanup any start/end listeners that were setup."""
self._cal_data = {
'all_day': False,
'offset_time': 0,
'message': '',
'start': None,
'end': None,
'location': None,
'description': None
}
def update(self):
"""Search for the next event."""
if not self.data or not self.data.update():
# update cached, don't do anything
return
if not self.data.event:
# we have no event to work on, make sure we're clean
self.cleanup()
return
def _get_date(date):
"""Get the dateTime from date or dateTime as a local."""
if 'date' in date:
return dt.as_utc(dt.dt.datetime.combine(
dt.parse_date(date['date']), dt.dt.time()))
else:
return dt.parse_datetime(date['dateTime'])
start = _get_date(self.data.event['start'])
end = _get_date(self.data.event['end'])
summary = self.data.event['summary']
# check if we have an offset tag in the message
# time is HH:MM or MM
reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset)
search = re.search(reg, summary)
if search and search.group(1):
time = search.group(1)
if ':' not in time:
if time[0] == '+' or time[0] == '-':
time = '{}0:{}'.format(time[0], time[1:])
else:
time = '0:{}'.format(time)
offset_time = time_period_str(time)
summary = (summary[:search.start()] + summary[search.end():]) \
.strip()
else:
offset_time = dt.dt.timedelta() # default it
# cleanup the string so we don't have a bunch of double+ spaces
self._cal_data['message'] = re.sub(' +', '', summary).strip()
self._cal_data['offset_time'] = offset_time
self._cal_data['location'] = self.data.event.get('location', '')
self._cal_data['description'] = self.data.event.get('description', '')
self._cal_data['start'] = start
self._cal_data['end'] = end
self._cal_data['all_day'] = 'date' in self.data.event['start']

View File

@@ -0,0 +1,82 @@
"""
Demo platform that has two fake binary sensors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import homeassistant.util.dt as dt_util
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo binary sensor platform."""
calendar_data_future = DemoGoogleCalendarDataFuture()
calendar_data_current = DemoGoogleCalendarDataCurrent()
add_devices([
DemoGoogleCalendar(hass, calendar_data_future, {
CONF_NAME: 'Future Event',
CONF_DEVICE_ID: 'future_event',
}),
DemoGoogleCalendar(hass, calendar_data_current, {
CONF_NAME: 'Current Event',
CONF_DEVICE_ID: 'current_event',
}),
])
class DemoGoogleCalendarData(object):
"""Setup base class for data."""
# pylint: disable=no-self-use
def update(self):
"""Return true so entity knows we have new data."""
return True
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
"""Setup future data event."""
def __init__(self):
"""Set the event to a future event."""
one_hour_from_now = dt_util.now() \
+ dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': one_hour_from_now.isoformat()
},
'end': {
'dateTime': (one_hour_from_now + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Future Event',
}
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
"""Create a current event we're in the middle of."""
def __init__(self):
"""Set the event data."""
middle_of_event = dt_util.now() \
- dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': middle_of_event.isoformat()
},
'end': {
'dateTime': (middle_of_event + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Current Event',
}
class DemoGoogleCalendar(CalendarEventDevice):
"""A Demo binary sensor."""
def __init__(self, hass, calendar_data, data):
"""The same as a google calendar but without the api calls."""
self.data = calendar_data
super().__init__(hass, data)

View File

@@ -0,0 +1,79 @@
"""
Support for Google Calendar Search binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.google_calendar/
"""
# pylint: disable=import-error
import logging
from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import (CONF_CAL_ID, CONF_ENTITIES,
CONF_TRACK, TOKEN_FILE,
GoogleCalendarService)
from homeassistant.util import Throttle, dt
DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime',
'maxResults': 1,
'singleEvents': True,
}
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, disc_info=None):
"""Setup the calendar platform for event devices."""
if disc_info is None:
return
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
disc_info[CONF_CAL_ID], data)
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
# pylint: disable=too-many-instance-attributes
class GoogleCalendarEventDevice(CalendarEventDevice):
"""A calendar event device."""
def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar,
data.get('search', None))
super().__init__(hass, data)
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
def __init__(self, calendar_service, calendar_id, search=None):
"""Setup how we are going to search the google calendar."""
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self.search = search
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
service = self.calendar_service.get()
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.utcnow().isoformat('T')
params['calendarId'] = self.calendar_id
if self.search:
params['q'] = self.search
events = service.events() # pylint: disable=no-member
result = events.list(**params).execute()
items = result.get('items', [])
self.event = items[0] if len(items) == 1 else None
return True

View File

@@ -101,7 +101,6 @@ class Camera(Entity):
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
response.enable_chunked_encoding()
yield from response.prepare(request)
def write(img_bytes):

View File

@@ -35,7 +35,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a FFmpeg Camera."""
if not async_run_test(hass, config.get(CONF_INPUT)):
return
hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
yield from async_add_devices([FFmpegCamera(hass, config)])
class FFmpegCamera(Camera):
@@ -75,7 +75,6 @@ class FFmpegCamera(Camera):
response = web.StreamResponse()
response.content_type = 'multipart/x-mixed-replace;boundary=ffserver'
response.enable_chunked_encoding()
yield from response.prepare(request)
@@ -86,7 +85,7 @@ class FFmpegCamera(Camera):
break
response.write(data)
finally:
self.hass.loop.create_task(stream.close())
self.hass.async_add_job(stream.close())
yield from response.write_eof()
@property

View File

@@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a generic IP Camera."""
hass.loop.create_task(async_add_devices([GenericCamera(hass, config)]))
yield from async_add_devices([GenericCamera(hass, config)])
class GenericCamera(Camera):
@@ -91,7 +91,7 @@ class GenericCamera(Camera):
if url == self._last_url and self._limit_refetch:
return self._last_image
# aiohttp don't support DigestAuth jet
# aiohttp don't support DigestAuth yet
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
def fetch():
"""Read image from a URL."""
@@ -109,15 +109,17 @@ class GenericCamera(Camera):
else:
try:
with async_timeout.timeout(10, loop=self.hass.loop):
respone = yield from self.hass.websession.get(
url,
auth=self._auth
)
self._last_image = yield from respone.read()
yield from respone.release()
response = yield from self.hass.websession.get(
url, auth=self._auth)
self._last_image = yield from response.read()
yield from response.release()
except asyncio.TimeoutError:
_LOGGER.error('Timeout getting camera image')
return self._last_image
except (aiohttp.errors.ClientError,
aiohttp.errors.ClientDisconnectedError) as err:
_LOGGER.error('Error getting new camera image: %s', err)
return self._last_image
self._last_url = url
return self._last_image

View File

@@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a MJPEG IP Camera."""
hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)]))
yield from async_add_devices([MjpegCamera(hass, config)])
def extract_image_from_mjpeg(stream):
@@ -112,7 +112,6 @@ class MjpegCamera(Camera):
response = web.StreamResponse()
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
response.enable_chunked_encoding()
yield from response.prepare(request)
@@ -123,7 +122,7 @@ class MjpegCamera(Camera):
break
response.write(data)
finally:
self.hass.loop.create_task(stream.release())
self.hass.async_add_job(stream.release())
yield from response.write_eof()
@property

View File

@@ -9,13 +9,15 @@ import logging
import voluptuous as vol
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout
from homeassistant.core import callback
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
@@ -57,6 +59,24 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a Synology IP Camera."""
if not config.get(CONF_VERIFY_SSL):
connector = aiohttp.TCPConnector(verify_ssl=False)
@asyncio.coroutine
def _async_close_connector(event):
"""Close websession on shutdown."""
yield from connector.close()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _async_close_connector)
else:
connector = hass.websession.connector
websession_init = aiohttp.ClientSession(
loop=hass.loop,
connector=connector
)
# Determine API to use for authentication
syno_api_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
@@ -69,13 +89,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
query_req = yield from hass.websession.get(
query_req = yield from websession_init.get(
syno_api_url,
params=query_payload,
verify_ssl=config.get(CONF_VERIFY_SSL)
params=query_payload
)
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", syno_api_url)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
query_resp = yield from query_req.json()
@@ -93,12 +112,26 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
session_id = yield from get_session_id(
hass,
websession_init,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
syno_auth_url,
config.get(CONF_VERIFY_SSL)
syno_auth_url
)
websession_init.detach()
# init websession
websession = aiohttp.ClientSession(
loop=hass.loop, connector=connector, cookies={'id': session_id})
@callback
def _async_close_websession(event):
"""Close websession on shutdown."""
websession.detach()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
# Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, camera_api)
@@ -110,14 +143,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
camera_req = yield from hass.websession.get(
camera_req = yield from websession.get(
syno_camera_url,
params=camera_payload,
verify_ssl=config.get(CONF_VERIFY_SSL),
cookies={'id': session_id}
params=camera_payload
)
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", syno_camera_url)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", syno_camera_url)
return False
camera_resp = yield from camera_req.json()
@@ -126,13 +157,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
# add cameras
devices = []
tasks = []
for camera in cameras:
if not config.get(CONF_WHITELIST):
camera_id = camera['id']
snapshot_path = camera['snapshot_path']
device = SynologyCamera(
hass,
websession,
config,
camera_id,
camera['name'],
@@ -141,15 +173,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
camera_path,
auth_path
)
tasks.append(device.async_read_sid())
devices.append(device)
yield from asyncio.gather(*tasks, loop=hass.loop)
hass.loop.create_task(async_add_devices(devices))
yield from async_add_devices(devices)
@asyncio.coroutine
def get_session_id(hass, username, password, login_url, valid_cert):
def get_session_id(hass, websession, username, password, login_url):
"""Get a session id."""
auth_payload = {
'api': AUTH_API,
@@ -162,13 +192,12 @@ def get_session_id(hass, username, password, login_url, valid_cert):
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
auth_req = yield from hass.websession.get(
auth_req = yield from websession.get(
login_url,
params=auth_payload,
verify_ssl=valid_cert
params=auth_payload
)
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", login_url)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", login_url)
return False
auth_resp = yield from auth_req.json()
@@ -180,36 +209,22 @@ def get_session_id(hass, username, password, login_url, valid_cert):
class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera."""
def __init__(self, config, camera_id, camera_name,
snapshot_path, streaming_path, camera_path, auth_path):
def __init__(self, hass, websession, config, camera_id,
camera_name, snapshot_path, streaming_path, camera_path,
auth_path):
"""Initialize a Synology Surveillance Station camera."""
super().__init__()
self.hass = hass
self._websession = websession
self._name = camera_name
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._synology_url = config.get(CONF_URL)
self._api_url = config.get(CONF_URL) + 'webapi/'
self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi'
self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID)
self._valid_cert = config.get(CONF_VERIFY_SSL)
self._camera_id = camera_id
self._snapshot_path = snapshot_path
self._streaming_path = streaming_path
self._camera_path = camera_path
self._auth_path = auth_path
self._session_id = None
@asyncio.coroutine
def async_read_sid(self):
"""Get a session id."""
self._session_id = yield from get_session_id(
self.hass,
self._username,
self._password,
self._login_url,
self._valid_cert
)
def camera_image(self):
"""Return bytes of camera image."""
@@ -230,14 +245,12 @@ class SynologyCamera(Camera):
}
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
response = yield from self.hass.websession.get(
response = yield from self._websession.get(
image_url,
params=image_payload,
verify_ssl=self._valid_cert,
cookies={'id': self._session_id}
params=image_payload
)
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", image_url)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", image_url)
return None
image = yield from response.read()
@@ -260,18 +273,16 @@ class SynologyCamera(Camera):
}
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
stream = yield from self.hass.websession.get(
stream = yield from self._websession.get(
streaming_url,
payload=streaming_payload,
verify_ssl=self._valid_cert,
cookies={'id': self._session_id}
params=streaming_payload
)
except asyncio.TimeoutError:
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", streaming_url)
raise HTTPGatewayTimeout()
response = web.StreamResponse()
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
response.enable_chunked_encoding()
yield from response.prepare(request)
@@ -282,7 +293,7 @@ class SynologyCamera(Camera):
break
response.write(data)
finally:
self.hass.loop.create_task(stream.release())
self.hass.async_add_job(stream.release())
yield from response.write_eof()
@property

View File

@@ -145,7 +145,7 @@ class GenericThermostat(ClimateDevice):
def max_temp(self):
"""Return the maximum temperature."""
# pylint: disable=no-member
if self._min_temp:
if self._max_temp:
return self._max_temp
else:
# Get default temp from super class
@@ -158,7 +158,7 @@ class GenericThermostat(ClimateDevice):
self._update_temp(new_state)
self._control_heating()
self.update_ha_state()
self.schedule_update_ha_state()
def _update_temp(self, state):
"""Update thermostat with latest state from sensor."""

View File

@@ -223,7 +223,6 @@ class HoneywellUSThermostat(ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
self._device.refresh()
return self._device.current_temperature
@property
@@ -274,3 +273,7 @@ class HoneywellUSThermostat(ClimateDevice):
"""Set the system mode (Cool, Heat, etc)."""
if hasattr(self._device, ATTR_SYSTEM_MODE):
self._device.system_mode = operation_mode
def update(self):
"""Update the state."""
self._device.refresh()

View File

@@ -56,6 +56,8 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
self._away = False # not yet supported
self._is_fan_on = False # not yet supported
self._current_temp = None
self._target_temp = None
@property
def should_poll(self):
@@ -70,16 +72,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value('temperature'))
return self._current_temp
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value('setpoint'))
return self._target_temp
def set_temperature(self, **kwargs):
"""Set new target temperature."""
@@ -94,3 +92,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
raise NotImplementedError()
def update(self):
"""Update KNX climate."""
from knxip.conversion import knx2_to_float
super().update()
self._current_temp = knx2_to_float(self.value('temperature'))
self._target_temp = knx2_to_float(self.value('setpoint'))

View File

@@ -24,7 +24,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors climate."""
if discovery_info is None:
return
for gateway in mysensors.GATEWAYS.values():
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
if not gateways:
return
for gateway in gateways:
if float(gateway.protocol_version) < 1.5:
continue
pres = gateway.const.Presentation

View File

@@ -12,7 +12,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['proliphix==0.4.0']
REQUIREMENTS = ['proliphix==0.4.1']
ATTR_FAN = 'fan'

View File

@@ -69,6 +69,8 @@ class RadioThermostat(ClimateDevice):
self._current_temperature = None
self._current_operation = STATE_IDLE
self._name = None
self._fmode = None
self._tmode = None
self.hold_temp = hold_temp
self.update()
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
@@ -87,8 +89,8 @@ class RadioThermostat(ClimateDevice):
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
ATTR_FAN: self.device.fmode['human'],
ATTR_MODE: self.device.tmode['human']
ATTR_FAN: self._fmode,
ATTR_MODE: self._tmode,
}
@property
@@ -115,10 +117,13 @@ class RadioThermostat(ClimateDevice):
"""Update the data from the thermostat."""
self._current_temperature = self.device.temp['raw']
self._name = self.device.name['raw']
if self.device.tmode['human'] == 'Cool':
self._fmode = self.device.fmode['human']
self._tmode = self.device.tmode['human']
if self._tmode == 'Cool':
self._target_temperature = self.device.t_cool['raw']
self._current_operation = STATE_COOL
elif self.device.tmode['human'] == 'Heat':
elif self._tmode == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_HEAT
else:
@@ -130,9 +135,9 @@ class RadioThermostat(ClimateDevice):
if temperature is None:
return
if self._current_operation == STATE_COOL:
self.device.t_cool = temperature
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_HEAT:
self.device.t_heat = temperature
self.device.t_heat = round(temperature * 2.0) / 2.0
if self.hold_temp:
self.device.hold = 1
else:
@@ -154,6 +159,6 @@ class RadioThermostat(ClimateDevice):
elif operation_mode == STATE_AUTO:
self.device.tmode = 3
elif operation_mode == STATE_COOL:
self.device.t_cool = self._target_temperature
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
elif operation_mode == STATE_HEAT:
self.device.t_heat = self._target_temperature
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0

View File

@@ -0,0 +1,331 @@
"""
Support for Wink thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.wink/
"""
from homeassistant.components.wink import WinkDevice
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_TEMPERATURE,
ATTR_CURRENT_HUMIDITY)
from homeassistant.const import (
TEMP_CELSIUS, STATE_ON,
STATE_OFF, STATE_UNKNOWN)
from homeassistant.loader import get_component
DEPENDENCIES = ['wink']
STATE_AUX = 'aux'
STATE_ECO = 'eco'
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
ATTR_SMART_TEMPERATURE = "smart_temperature"
ATTR_ECO_TARGET = "eco_target"
ATTR_OCCUPIED = "occupied"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink thermostat."""
import pywink
temp_unit = hass.config.units.temperature_unit
add_devices(WinkThermostat(thermostat, temp_unit)
for thermostat in pywink.get_thermostats())
# pylint: disable=abstract-method,too-many-public-methods, too-many-branches
class WinkThermostat(WinkDevice, ClimateDevice):
"""Representation of a Wink thermostat."""
def __init__(self, wink, temp_unit):
"""Initialize the Wink device."""
super().__init__(wink)
wink = get_component('wink')
self._config_temp_unit = temp_unit
@property
def temperature_unit(self):
"""Return the unit of measurement."""
# The Wink API always returns temp in Celsius
return TEMP_CELSIUS
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
data = {}
target_temp_high = self.target_temperature_high
target_temp_low = self.target_temperature_low
if target_temp_high is not None:
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
self.target_temperature_high)
if target_temp_low is not None:
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
self.target_temperature_low)
if self.external_temperature:
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display(
self.external_temperature)
if self.smart_temperature:
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
if self.occupied:
data[ATTR_OCCUPIED] = self.occupied
if self.eco_target:
data[ATTR_ECO_TARGET] = self.eco_target
current_humidity = self.current_humidity
if current_humidity is not None:
data[ATTR_CURRENT_HUMIDITY] = current_humidity
return data
@property
def current_temperature(self):
"""Return the current temperature."""
return self.wink.current_temperature()
@property
def current_humidity(self):
"""Return the current humidity."""
if self.wink.current_humidity() is not None:
# The API states humidity will be a float 0-1
# the only example API response with humidity listed show an int
# This will address both possibilities
if self.wink.current_humidity() < 1:
return self.wink.current_humidity() * 100
else:
return self.wink.current_humidity()
@property
def external_temperature(self):
"""Return the current external temperature."""
return self.wink.current_external_temperature()
@property
def smart_temperature(self):
"""Return the current average temp of all remote sensor."""
return self.wink.current_smart_temperature()
@property
def eco_target(self):
"""Return status of eco target (Is the termostat in eco mode)."""
return self.wink.eco_target()
@property
def occupied(self):
"""Return status of if the thermostat has detected occupancy."""
return self.wink.occupied()
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
if not self.wink.is_on():
current_op = STATE_OFF
elif self.wink.current_hvac_mode() == 'cool_only':
current_op = STATE_COOL
elif self.wink.current_hvac_mode() == 'heat_only':
current_op = STATE_HEAT
elif self.wink.current_hvac_mode() == 'aux':
current_op = STATE_HEAT
elif self.wink.current_hvac_mode() == 'auto':
current_op = STATE_AUTO
elif self.wink.current_hvac_mode() == 'eco':
current_op = STATE_ECO
else:
current_op = STATE_UNKNOWN
return current_op
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
target_hum = None
if self.wink.current_humidifier_mode() == 'on':
if self.wink.current_humidifier_set_point() is not None:
target_hum = self.wink.current_humidifier_set_point() * 100
elif self.wink.current_dehumidifier_mode() == 'on':
if self.wink.current_dehumidifier_set_point() is not None:
target_hum = self.wink.current_dehumidifier_set_point() * 100
else:
target_hum = None
return target_hum
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self.current_operation != STATE_AUTO and not self.is_away_mode_on:
if self.current_operation == STATE_COOL:
return self.wink.current_max_set_point()
elif self.current_operation == STATE_HEAT:
return self.wink.current_min_set_point()
else:
return None
else:
return None
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
if self.current_operation == STATE_AUTO:
return self.wink.current_min_set_point()
return None
@property
def target_temperature_high(self):
"""Return the higher bound temperature we try to reach."""
if self.current_operation == STATE_AUTO:
return self.wink.current_max_set_point()
return None
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self.wink.away()
@property
def is_aux_heat_on(self):
"""Return true if aux heater."""
if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on():
return True
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
return False
else:
return None
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp is not None:
if self.current_operation == STATE_COOL:
target_temp_high = target_temp
if self.current_operation == STATE_HEAT:
target_temp_low = target_temp
if target_temp_low is not None:
target_temp_low = target_temp_low
if target_temp_high is not None:
target_temp_high = target_temp_high
self.wink.set_temperature(target_temp_low, target_temp_high)
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
if operation_mode == STATE_HEAT:
self.wink.set_operation_mode('heat_only')
elif operation_mode == STATE_COOL:
self.wink.set_operation_mode('cool_only')
elif operation_mode == STATE_AUTO:
self.wink.set_operation_mode('auto')
elif operation_mode == STATE_OFF:
self.wink.set_operation_mode('off')
elif operation_mode == STATE_AUX:
self.wink.set_operation_mode('aux')
elif operation_mode == STATE_ECO:
self.wink.set_operation_mode('eco')
@property
def operation_list(self):
"""List of available operation modes."""
op_list = ['off']
modes = self.wink.hvac_modes()
if 'cool_only' in modes:
op_list.append(STATE_COOL)
if 'heat_only' in modes or 'aux' in modes:
op_list.append(STATE_HEAT)
if 'auto' in modes:
op_list.append(STATE_AUTO)
if 'eco' in modes:
op_list.append(STATE_ECO)
return op_list
def turn_away_mode_on(self):
"""Turn away on."""
self.wink.set_away_mode()
def turn_away_mode_off(self):
"""Turn away off."""
self.wink.set_away_mode(False)
@property
def current_fan_mode(self):
"""Return whether the fan is on."""
if self.wink.current_fan_mode() == 'on':
return STATE_ON
elif self.wink.current_fan_mode() == 'auto':
return STATE_AUTO
else:
# No Fan available so disable slider
return None
@property
def fan_list(self):
"""List of available fan modes."""
if self.wink.has_fan():
return self.wink.fan_modes()
return None
def set_fan_mode(self, fan):
"""Turn fan on/off."""
self.wink.set_fan_mode(fan.lower())
def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
self.set_operation_mode(STATE_AUX)
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
self.set_operation_mode(STATE_AUTO)
@property
def min_temp(self):
"""Return the minimum temperature."""
minimum = 7 # Default minimum
min_min = self.wink.min_min_set_point()
min_max = self.wink.min_max_set_point()
return_value = minimum
if self.current_operation == STATE_HEAT:
if min_min:
return_value = min_min
else:
return_value = minimum
elif self.current_operation == STATE_COOL:
if min_max:
return_value = min_max
else:
return_value = minimum
elif self.current_operation == STATE_AUTO:
if min_min and min_max:
return_value = min(min_min, min_max)
else:
return_value = minimum
else:
return_value = minimum
return return_value
@property
def max_temp(self):
"""Return the maximum temperature."""
maximum = 35 # Default maximum
max_min = self.wink.max_min_set_point()
max_max = self.wink.max_max_set_point()
return_value = maximum
if self.current_operation == STATE_HEAT:
if max_min:
return_value = max_min
else:
return_value = maximum
elif self.current_operation == STATE_COOL:
if max_max:
return_value = max_max
else:
return_value = maximum
elif self.current_operation == STATE_AUTO:
if max_min and max_max:
return_value = min(max_min, max_max)
else:
return_value = maximum
else:
return_value = maximum
return return_value

View File

@@ -1,5 +1,5 @@
"""
Support for ZWave climate devices.
Support for Z-Wave climate devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.zwave/
@@ -8,8 +8,7 @@ https://home-assistant.io/components/climate.zwave/
# pylint: disable=import-error
import logging
from homeassistant.components.climate import DOMAIN
from homeassistant.components.climate import (
ClimateDevice, ATTR_OPERATION_MODE)
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.const import (
@@ -18,44 +17,23 @@ from homeassistant.const import (
_LOGGER = logging.getLogger(__name__)
CONF_NAME = 'name'
DEFAULT_NAME = 'ZWave Climate'
DEFAULT_NAME = 'Z-Wave Climate'
REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
HORSTMANN = 0x0059
HORSTMANN_HRT4_ZW = 0x3
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
ATTR_OPERATING_STATE = 'operating_state'
ATTR_FAN_STATE = 'fan_state'
WORKAROUND_ZXT_120 = 'zxt_120'
WORKAROUND_HRT4_ZW = 'hrt4_zw'
DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120,
HORSTMANN_HRT4_ZW_THERMOSTAT: WORKAROUND_HRT4_ZW
}
SET_TEMP_TO_INDEX = {
'Heat': 1,
'Cool': 2,
'Auto': 3,
'Aux Heat': 4,
'Resume': 5,
'Fan Only': 6,
'Furnace': 7,
'Dry Air': 8,
'Moist Air': 9,
'Auto Changeover': 10,
'Heat Econ': 11,
'Cool Econ': 12,
'Away': 13,
'Unknown': 14
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ZWave Climate devices."""
"""Set up the Z-Wave Climate devices."""
if discovery_info is None or zwave.NETWORK is None:
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
discovery_info, zwave.NETWORK)
@@ -70,13 +48,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Represents a ZWave Climate device."""
"""Representation of a Z-Wave Climate device."""
def __init__(self, value, temp_unit):
"""Initialize the zwave climate device."""
"""Initialize the Z-Wave climate device."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._index = value.index
self._node = value.node
self._target_temperature = None
self._current_temperature = None
@@ -85,13 +64,12 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._operating_state = None
self._current_fan_mode = None
self._fan_list = None
self._fan_state = None
self._current_swing_mode = None
self._swing_list = None
self._unit = temp_unit
self._index_operation = None
_LOGGER.debug("temp_unit is %s", self._unit)
self._zxt_120 = None
self._hrt4_zw = None
self.update_properties()
# register listener
dispatcher.connect(
@@ -106,17 +84,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
" workaround")
self._zxt_120 = 1
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_HRT4_ZW:
_LOGGER.debug("Horstmann HRT4-ZW Zwave Thermostat"
" workaround")
self._hrt4_zw = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
self.update_ha_state()
self.schedule_update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
@@ -125,23 +99,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
for value in self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
self._current_operation = value.data
self._index_operation = SET_TEMP_TO_INDEX.get(
self._current_operation)
self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list)
_LOGGER.debug("self._current_operation=%s",
self._current_operation)
# Current Temp
for value in (self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
.values()):
for value in (
self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
.values()):
if value.label == 'Temperature':
self._current_temperature = int(value.data)
self._current_temperature = round((float(value.data)), 1)
self._unit = value.units
# Fan Mode
for value in (self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
.values()):
for value in (
self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
.values()):
self._current_fan_mode = value.data
self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list)
@@ -149,9 +123,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._current_fan_mode)
# Swing mode
if self._zxt_120 == 1:
for value in (self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
.values()):
for value in (
self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
.values()):
if value.command_class == \
zwave.const.COMMAND_CLASS_CONFIGURATION and \
value.index == 33:
@@ -161,35 +136,39 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
_LOGGER.debug("self._current_swing_mode=%s",
self._current_swing_mode)
# Set point
for value in (self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
.values()):
if value.data == 0:
_LOGGER.debug("Setpoint is 0, setting default to "
"current_temperature=%s",
self._current_temperature)
self._target_temperature = int(self._current_temperature)
break
if self.current_operation is not None and \
self.current_operation != 'Off':
if self._index_operation != value.index:
continue
if self._zxt_120:
temps = []
for value in (
self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
.values()):
temps.append((round(float(value.data)), 1))
if value.index == self._index:
if value.data == 0:
_LOGGER.debug("Setpoint is 0, setting default to "
"current_temperature=%s",
self._current_temperature)
self._target_temperature = (
round((float(self._current_temperature)), 1))
break
self._target_temperature = int(value.data)
break
_LOGGER.debug("Device can't set setpoint based on operation mode."
" Defaulting to index=1")
self._target_temperature = int(value.data)
else:
self._target_temperature = round((float(value.data)), 1)
# Operating state
for value in (self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE)
.values()):
for value in (
self._node.get_values(
class_id=zwave.const
.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()):
self._operating_state = value.data
# Fan operating state
for value in (
self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE)
.values()):
self._fan_state = value.data
@property
def should_poll(self):
"""No polling on ZWave."""
"""No polling on Z-Wave."""
return False
@property
@@ -248,53 +227,19 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
temperature = kwargs.get(ATTR_TEMPERATURE)
else:
return
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
for value in (self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
.values()):
if operation_mode is not None:
setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode)
if value.index != setpoint_mode:
continue
_LOGGER.debug("setpoint_mode=%s", setpoint_mode)
value.data = temperature
break
if self.current_operation is not None:
if self._hrt4_zw and self.current_operation == 'Off':
# HRT4-ZW can change setpoint when off.
value.data = int(temperature)
if self._index_operation != value.index:
continue
_LOGGER.debug("self._index_operation=%s and"
" self._current_operation=%s",
self._index_operation,
self._current_operation)
if value.index == self._index:
if self._zxt_120:
_LOGGER.debug("zxt_120: Setting new setpoint for %s, "
" operation=%s, temp=%s",
self._index_operation,
self._current_operation, temperature)
# ZXT-120 does not support get setpoint
self._target_temperature = temperature
# ZXT-120 responds only to whole int
value.data = round(temperature, 0)
self._target_temperature = temperature
self.update_ha_state()
break
else:
_LOGGER.debug("Setting new setpoint for %s, "
"operation=%s, temp=%s",
self._index_operation,
self._current_operation, temperature)
value.data = temperature
break
else:
_LOGGER.debug("Setting new setpoint for no known "
"operation mode. Index=1 and "
"temperature=%s", temperature)
value.data = temperature
self.update_ha_state()
break
def set_fan_mode(self, fan):
@@ -331,9 +276,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
data = super().device_state_attributes
if self._operating_state:
return {
"operating_state": self._operating_state,
}
else:
return {}
data[ATTR_OPERATING_STATE] = self._operating_state,
if self._fan_state:
data[ATTR_FAN_STATE] = self._fan_state
return data

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['fuzzywuzzy==0.12.0']
REQUIREMENTS = ['fuzzywuzzy==0.14.0']
ATTR_TEXT = 'text'

View File

@@ -8,6 +8,7 @@ import logging
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt
from homeassistant.components.cover import CoverDevice
from homeassistant.const import (
@@ -89,29 +90,30 @@ class MqttCover(CoverDevice):
self._retain = retain
self._optimistic = optimistic or state_topic is None
@callback
def message_received(topic, payload, qos):
"""A new MQTT message has been received."""
if value_template is not None:
payload = value_template.render_with_possible_json_value(
payload = value_template.async_render_with_possible_json_value(
payload)
if payload == self._state_open:
self._state = False
_LOGGER.warning("state=%s", int(self._state))
self.update_ha_state()
hass.async_add_job(self.async_update_ha_state())
elif payload == self._state_closed:
self._state = True
self.update_ha_state()
hass.async_add_job(self.async_update_ha_state())
elif payload.isnumeric() and 0 <= int(payload) <= 100:
if int(payload) > 0:
self._state = False
else:
self._state = True
self._position = int(payload)
self.update_ha_state()
hass.async_add_job(self.async_update_ha_state())
else:
_LOGGER.warning(
"Payload is not True, False, or integer (0-100): %s",
payload)
if self._state_topic is None:
# Force into optimistic mode.
self._optimistic = True

View File

@@ -18,7 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors platform for covers."""
if discovery_info is None:
return
for gateway in mysensors.GATEWAYS.values():
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
if not gateways:
return
for gateway in gateways:
pres = gateway.const.Presentation
set_req = gateway.const.SetReq
map_sv_types = {

View File

@@ -36,15 +36,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
and value.index == 0):
value.set_change_verified(False)
add_devices([ZwaveRollershutter(value)])
elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL:
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
value.command_class ==
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
if (value.type != zwave.const.TYPE_BOOL and
value.genre != zwave.const.GENRE_USER):
return
value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)])
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
if (value.type != zwave.const.TYPE_BOOL and
value.genre != zwave.const.GENRE_USER):
return
value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)])
else:
return

View File

@@ -17,6 +17,7 @@ DOMAIN = 'demo'
COMPONENTS_WITH_DEMO_PLATFORM = [
'alarm_control_panel',
'binary_sensor',
'calendar',
'camera',
'climate',
'cover',

View File

@@ -9,6 +9,7 @@ from datetime import timedelta
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.helpers.event import track_point_in_time
@@ -79,21 +80,22 @@ def setup(hass, config):
return None
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
def turn_light_on_before_sunset(light_id):
def async_turn_on_before_sunset(light_id):
"""Helper function to turn on lights.
Speed is slow if there are devices home and the light is not on yet.
"""
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
return
light.turn_on(hass, light_id,
transition=LIGHT_TRANSITION_TIME.seconds,
profile=light_profile)
light.async_turn_on(hass, light_id,
transition=LIGHT_TRANSITION_TIME.seconds,
profile=light_profile)
# Track every time sun rises so we can schedule a time-based
# pre-sun set event
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
sun.STATE_ABOVE_HORIZON)
@callback
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
"""The moment sun sets we want to have all the lights on.
@@ -104,16 +106,21 @@ def setup(hass, config):
if not start_point:
return
def turn_on(light_id):
def async_turn_on_factory(light_id):
"""Lambda can keep track of function parameters.
No local parameters. If we put the lambda directly in the below
statement only the last light will be turned on.
"""
return lambda now: turn_light_on_before_sunset(light_id)
@callback
def async_turn_on_light(now):
"""Turn on specific light."""
async_turn_on_before_sunset(light_id)
return async_turn_on_light
for index, light_id in enumerate(light_ids):
track_point_in_time(hass, turn_on(light_id),
track_point_in_time(hass, async_turn_on_factory(light_id),
start_point + index * LIGHT_TRANSITION_TIME)
# If the sun is already above horizon schedule the time-based pre-sun set
@@ -122,6 +129,7 @@ def setup(hass, config):
schedule_lights_at_sun_set(hass, None, None, None)
@track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME)
@callback
def check_light_on_dev_state_change(hass, entity, old_state, new_state):
"""Handle tracked device state changes."""
# pylint: disable=unused-variable
@@ -136,7 +144,7 @@ def setup(hass, config):
# Do we need lights?
if light_needed:
logger.info("Home coming event for %s. Turning lights on", entity)
light.turn_on(hass, light_ids, profile=light_profile)
light.async_turn_on(hass, light_ids, profile=light_profile)
# Are we in the time span were we would turn on the lights
# if someone would be home?
@@ -149,7 +157,7 @@ def setup(hass, config):
# when the fading in started and turn it on if so
for index, light_id in enumerate(light_ids):
if now > start_point + index * LIGHT_TRANSITION_TIME:
light.turn_on(hass, light_id)
light.async_turn_on(hass, light_id)
else:
# If this light didn't happen to be turned on yet so
@@ -158,6 +166,7 @@ def setup(hass, config):
if not disable_turn_off:
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
@callback
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
"""Handle device group state change."""
# pylint: disable=unused-variable
@@ -166,6 +175,6 @@ def setup(hass, config):
logger.info(
"Everyone has left but there are lights on. Turning them off")
light.turn_off(hass, light_ids)
light.async_turn_off(hass, light_ids)
return True

View File

@@ -8,13 +8,13 @@ import asyncio
from datetime import timedelta
import logging
import os
import threading
from typing import Any, Sequence, Callable
import voluptuous as vol
from homeassistant.bootstrap import (
prepare_setup_platform, log_exception)
async_prepare_setup_platform, async_log_exception)
from homeassistant.core import callback
from homeassistant.components import group, zone
from homeassistant.components.discovery import SERVICE_NETGEAR
from homeassistant.config import load_yaml_config_file
@@ -28,10 +28,10 @@ from homeassistant.util.async import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.helpers.event import async_track_utc_time_change
from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
DOMAIN = 'device_tracker'
DEPENDENCIES = ['zone']
@@ -106,14 +106,15 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
hass.services.call(DOMAIN, SERVICE_SEE, data)
def setup(hass: HomeAssistantType, config: ConfigType):
@asyncio.coroutine
def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Setup device tracker."""
yaml_path = hass.config.path(YAML_DEVICES)
try:
conf = config.get(DOMAIN, [])
except vol.Invalid as ex:
log_exception(ex, DOMAIN, config, hass)
async_log_exception(ex, DOMAIN, config, hass)
return False
else:
conf = conf[0] if len(conf) > 0 else {}
@@ -121,60 +122,77 @@ def setup(hass: HomeAssistantType, config: ConfigType):
timedelta(seconds=DEFAULT_CONSIDER_HOME))
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
devices = load_config(yaml_path, hass, consider_home)
devices = yield from async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(hass, consider_home, track_new, devices)
def setup_platform(p_type, p_config, disc_info=None):
# update tracked devices
update_tasks = [device.async_update_ha_state() for device in devices
if device.track]
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
@asyncio.coroutine
def async_setup_platform(p_type, p_config, disc_info=None):
"""Setup a device tracker platform."""
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
platform = yield from async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
if platform is None:
return
try:
if hasattr(platform, 'get_scanner'):
scanner = platform.get_scanner(hass, {DOMAIN: p_config})
scanner = yield from hass.loop.run_in_executor(
None, platform.get_scanner, hass, {DOMAIN: p_config})
if scanner is None:
_LOGGER.error('Error setting up platform %s', p_type)
return
setup_scanner_platform(hass, p_config, scanner, tracker.see)
yield from async_setup_scanner_platform(
hass, p_config, scanner, tracker.async_see)
return
if not platform.setup_scanner(hass, p_config, tracker.see):
ret = yield from hass.loop.run_in_executor(
None, platform.setup_scanner, hass, p_config, tracker.see)
if not ret:
_LOGGER.error('Error setting up platform %s', p_type)
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up platform %s', p_type)
for p_type, p_config in config_per_platform(config, DOMAIN):
setup_platform(p_type, p_config)
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
in config_per_platform(config, DOMAIN)]
if setup_tasks:
yield from asyncio.wait(setup_tasks, loop=hass.loop)
def device_tracker_discovered(service, info):
yield from tracker.async_setup_group()
@callback
def async_device_tracker_discovered(service, info):
"""Called when a device tracker platform is discovered."""
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
hass.async_add_job(
async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info))
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
device_tracker_discovered)
discovery.async_listen(
hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered)
def update_stale(now):
"""Clean up stale devices."""
tracker.update_stale(now)
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
# Clean up stale devices
async_track_utc_time_change(
hass, tracker.async_update_stale, second=range(0, 60, 5))
tracker.setup_group()
def see_service(call):
@asyncio.coroutine
def async_see_service(call):
"""Service to see a device."""
args = {key: value for key, value in call.data.items() if key in
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
tracker.see(**args)
yield from tracker.async_see(**args)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
descriptions.get(SERVICE_SEE))
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register(
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
return True
@@ -188,91 +206,116 @@ class DeviceTracker(object):
self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home
self.track_new = track_new
self.group = None # type: group.Group
self._is_updating = asyncio.Lock(loop=hass.loop)
for dev in devices:
if self.devices[dev.dev_id] is not dev:
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
_LOGGER.warning('Duplicate device MAC addresses detected %s',
dev.mac)
self.consider_home = consider_home
self.track_new = track_new
self.lock = threading.Lock()
for device in devices:
if device.track:
device.update_ha_state()
self.group = None # type: group.Group
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
battery: str=None, attributes: dict=None):
"""Notify the device tracker that you see a device."""
with self.lock:
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
elif mac is not None:
mac = str(mac).upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
else:
dev_id = cv.slug(str(dev_id).lower())
device = self.devices.get(dev_id)
self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps,
gps_accuracy, battery, attributes)
)
if device:
device.seen(host_name, location_name, gps, gps_accuracy,
battery, attributes)
if device.track:
device.update_ha_state()
return
@asyncio.coroutine
def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None,
location_name: str=None, gps: GPSType=None,
gps_accuracy=None, battery: str=None, attributes: dict=None):
"""Notify the device tracker that you see a device.
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device(
self.hass, self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '))
self.devices[dev_id] = device
if mac is not None:
self.mac_to_dev[mac] = device
device.seen(host_name, location_name, gps, gps_accuracy, battery,
attributes)
This method is a coroutine.
"""
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
elif mac is not None:
mac = str(mac).upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
else:
dev_id = cv.slug(str(dev_id).lower())
device = self.devices.get(dev_id)
if device:
yield from device.async_seen(host_name, location_name, gps,
gps_accuracy, battery, attributes)
if device.track:
device.update_ha_state()
yield from device.async_update_ha_state()
return
self.hass.bus.async_fire(EVENT_NEW_DEVICE, device)
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device(
self.hass, self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '))
self.devices[dev_id] = device
if mac is not None:
self.mac_to_dev[mac] = device
# During init, we ignore the group
if self.group is not None:
self.group.update_tracked_entity_ids(
list(self.group.tracking) + [device.entity_id])
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
yield from device.async_seen(host_name, location_name, gps,
gps_accuracy, battery, attributes)
def setup_group(self):
"""Initialize group for all tracked devices."""
run_coroutine_threadsafe(
self.async_setup_group(), self.hass.loop).result()
if device.track:
yield from device.async_update_ha_state()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
})
# During init, we ignore the group
if self.group is not None:
yield from self.group.async_update_tracked_entity_ids(
list(self.group.tracking) + [device.entity_id])
# update known_devices.yaml
self.hass.async_add_job(
self.async_update_config(self.hass.config.path(YAML_DEVICES),
dev_id, device)
)
@asyncio.coroutine
def async_update_config(self, path, dev_id, device):
"""Add device to YAML configuration file.
This method is a coroutine.
"""
with (yield from self._is_updating):
self.hass.loop.run_in_executor(
None, update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device)
@asyncio.coroutine
def async_setup_group(self):
"""Initialize group for all tracked devices.
This method must be run in the event loop.
This method is a coroutine.
"""
entity_ids = (dev.entity_id for dev in self.devices.values()
if dev.track)
self.group = yield from group.Group.async_create_group(
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
def update_stale(self, now: dt_util.dt.datetime):
"""Update stale devices."""
with self.lock:
for device in self.devices.values():
if (device.track and device.last_update_home and
device.stale(now)):
device.update_ha_state(True)
@callback
def async_update_stale(self, now: dt_util.dt.datetime):
"""Update stale devices.
This method must be run in the event loop.
"""
for device in self.devices.values():
if (device.track and device.last_update_home) and \
device.stale(now):
self.hass.async_add_job(device.async_update_ha_state(True))
class Device(Entity):
@@ -359,9 +402,10 @@ class Device(Entity):
"""If device should be hidden."""
return self.away_hide and self.state != STATE_HOME
def seen(self, host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=0, battery: str=None,
attributes: dict=None):
@asyncio.coroutine
def async_seen(self, host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=0, battery: str=None,
attributes: dict=None):
"""Mark the device as seen."""
self.last_seen = dt_util.utcnow()
self.host_name = host_name
@@ -370,28 +414,38 @@ class Device(Entity):
self.battery = battery
self.attributes = attributes
self.gps = None
if gps is not None:
try:
self.gps = float(gps[0]), float(gps[1])
except (ValueError, TypeError, IndexError):
_LOGGER.warning('Could not parse gps value for %s: %s',
self.dev_id, gps)
self.update()
# pylint: disable=not-an-iterable
yield from self.async_update()
def stale(self, now: dt_util.dt.datetime=None):
"""Return if device state is stale."""
"""Return if device state is stale.
Async friendly.
"""
return self.last_seen and \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
def update(self):
"""Update state of entity."""
@asyncio.coroutine
def async_update(self):
"""Update state of entity.
This method is a coroutine.
"""
if not self.last_seen:
return
elif self.location_name:
self._state = self.location_name
elif self.gps is not None:
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
self.gps_accuracy)
zone_state = zone.async_active_zone(
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
if zone_state is None:
self._state = STATE_NOT_HOME
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
@@ -409,6 +463,17 @@ class Device(Entity):
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file."""
return run_coroutine_threadsafe(
async_load_config(path, hass, consider_home), hass.loop).result()
@asyncio.coroutine
def async_load_config(path: str, hass: HomeAssistantType,
consider_home: timedelta):
"""Load devices from YAML configuration file.
This method is a coroutine.
"""
dev_schema = vol.Schema({
vol.Required('name'): cv.string,
vol.Optional('track', default=False): cv.boolean,
@@ -423,7 +488,8 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
try:
result = []
try:
devices = load_yaml_config_file(path)
devices = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, path)
except HomeAssistantError as err:
_LOGGER.error('Unable to load %s: %s', path, str(err))
return []
@@ -433,7 +499,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id)
except vol.Invalid as exp:
log_exception(exp, dev_id, devices, hass)
async_log_exception(exp, dev_id, devices, hass)
else:
result.append(Device(hass, **device))
return result
@@ -442,9 +508,13 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
return []
def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, see_device: Callable):
"""Helper method to connect scanner-based platform to device tracker."""
@asyncio.coroutine
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, async_see_device: Callable):
"""Helper method to connect scanner-based platform to device tracker.
This method is a coroutine.
"""
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
# Initial scan of each mac we also tell about host name for config
@@ -452,18 +522,20 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
def device_tracker_scan(now: dt_util.dt.datetime):
"""Called when interval matches."""
for mac in scanner.scan_devices():
found_devices = scanner.scan_devices()
for mac in found_devices:
if mac in seen:
host_name = None
else:
host_name = scanner.get_device_name(mac)
seen.add(mac)
see_device(mac=mac, host_name=host_name)
hass.async_add_job(async_see_device(mac=mac, host_name=host_name))
track_utc_time_change(hass, device_tracker_scan, second=range(0, 60,
interval))
async_track_utc_time_change(
hass, device_tracker_scan, second=range(0, 60, interval))
device_tracker_scan(None)
hass.async_add_job(device_tracker_scan, None)
def update_config(path: str, dev_id: str, device: Device):
@@ -481,7 +553,10 @@ def update_config(path: str, dev_id: str, device: Device):
def get_gravatar_for_email(email: str):
"""Return an 80px Gravatar for the given email address."""
"""Return an 80px Gravatar for the given email address.
Async friendly.
"""
import hashlib
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())

View File

@@ -42,6 +42,7 @@ def get_scanner(hass, config):
scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "ip", "last_update"])

View File

@@ -76,6 +76,15 @@ _IP_NEIGH_REGEX = re.compile(
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' +
r'(?P<status>(\w+))')
_NVRAM_CMD = 'nvram get client_info_tmp'
_NVRAM_REGEX = re.compile(
r'.*>.*>' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
r'>' +
r'(?P<mac>(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' +
r'>' +
r'.*')
# pylint: disable=unused-argument
def get_scanner(hass, config):
@@ -84,7 +93,8 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
class AsusWrtDeviceScanner(object):
@@ -155,7 +165,8 @@ class AsusWrtDeviceScanner(object):
active_clients = [client for client in data.values() if
client['status'] == 'REACHABLE' or
client['status'] == 'DELAY' or
client['status'] == 'STALE']
client['status'] == 'STALE' or
client['status'] == 'IN_NVRAM']
self.last_results = active_clients
return True
@@ -184,13 +195,18 @@ class AsusWrtDeviceScanner(object):
ssh.sendline(_WL_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
ssh.sendline(_NVRAM_CMD)
ssh.prompt()
nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:]
else:
arp_result = ['']
nvram_result = ['']
ssh.sendline(_LEASES_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
ssh.logout()
return AsusWrtResult(neighbors, leases_result, arp_result)
return AsusWrtResult(neighbors, leases_result, arp_result,
nvram_result)
except pxssh.ExceptionPxssh as exc:
_LOGGER.error('Unexpected response from router: %s', exc)
return None
@@ -213,13 +229,18 @@ class AsusWrtDeviceScanner(object):
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
leases_result = (telnet.read_until(prompt_string).
split(b'\n')[1:-1])
telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
nvram_result = (telnet.read_until(prompt_string).
split(b'\n')[1].split(b'<')[1:])
else:
arp_result = ['']
nvram_result = ['']
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = (telnet.read_until(prompt_string).
split(b'\n')[1:-1])
telnet.write('exit\n'.encode('ascii'))
return AsusWrtResult(neighbors, leases_result, arp_result)
return AsusWrtResult(neighbors, leases_result, arp_result,
nvram_result)
except EOFError:
_LOGGER.error('Unexpected response from router')
return None
@@ -277,6 +298,26 @@ class AsusWrtDeviceScanner(object):
'ip': arp_match.group('ip'),
'mac': match.group('mac').upper(),
}
# match mac addresses to IP addresses in NVRAM table
for nvr in result.nvram:
if match.group('mac').upper() in nvr.decode('utf-8'):
nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8'))
if not nvram_match:
_LOGGER.warning('Could not parse nvr row: %s', nvr)
continue
# skip current check if already in ARP table
if nvram_match.group('ip') in devices.keys():
continue
devices[nvram_match.group('ip')] = {
'host': host,
'status': 'IN_NVRAM',
'ip': nvram_match.group('ip'),
'mac': match.group('mac').upper(),
}
else:
for lease in result.leases:
match = _LEASES_REGEX.search(lease.decode('utf-8'))

View File

@@ -0,0 +1,162 @@
"""
Support for Cisco IOS Routers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.cisco_ios/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
CONF_PORT
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1']
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=''): cv.string,
vol.Optional(CONF_PORT): cv.port,
})
)
def get_scanner(hass, config):
"""Validate the configuration and return a Cisco scanner."""
scanner = CiscoDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class CiscoDeviceScanner(object):
"""This class queries a wireless router running Cisco IOS firmware."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.port = config.get(CONF_PORT)
self.password = config.get(CONF_PASSWORD)
self.last_results = {}
self.success_init = self._update_info()
_LOGGER.info('cisco_ios scanner initialized')
# pylint: disable=no-self-use
def get_device_name(self, device):
"""The firmware doesn't save the name of the wireless device."""
return None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return self.last_results
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Ensure the information from the Cisco router is up to date.
Returns boolean if scanning successful.
"""
string_result = self._get_arp_data()
if string_result:
self.last_results = []
last_results = []
lines_result = string_result.splitlines()
# Remove the first two lines, as they contains the arp command
# and the arp table titles e.g.
# show ip arp
# Protocol Address | Age (min) | Hardware Addr | Type | Interface
lines_result = lines_result[2:]
for line in lines_result:
if len(line.split()) is 6:
parts = line.split()
if len(parts) != 6:
continue
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
# 'GigabitEthernet0']
age = parts[2]
hw_addr = parts[3]
if age != "-":
mac = _parse_cisco_mac_address(hw_addr)
age = int(age)
if age < 1:
last_results.append(mac)
self.last_results = last_results
return True
return False
def _get_arp_data(self):
"""Open connection to the router and get arp entries."""
from pexpect import pxssh
import re
try:
cisco_ssh = pxssh.pxssh()
cisco_ssh.login(self.host, self.username, self.password,
port=self.port, auto_prompt_reset=False)
# Find the hostname
initial_line = cisco_ssh.before.decode('utf-8').splitlines()
router_hostname = initial_line[len(initial_line) - 1]
router_hostname += "#"
# Set the discovered hostname as prompt
regex_expression = ('(?i)^%s' % router_hostname).encode()
cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE)
# Allow full arp table to print at once
cisco_ssh.sendline("terminal length 0")
cisco_ssh.prompt(1)
cisco_ssh.sendline("show ip arp")
cisco_ssh.prompt(1)
devices_result = cisco_ssh.before
return devices_result.decode("utf-8")
except pxssh.ExceptionPxssh as px_e:
_LOGGER.error("pxssh failed on login.")
_LOGGER.error(px_e)
return None
def _parse_cisco_mac_address(cisco_hardware_addr):
"""
Parse a Cisco formatted HW address to normal MAC.
e.g. convert
001d.ec02.07ab
to:
00:1D:EC:02:07:AB
Takes in cisco_hwaddr: HWAddr String from Cisco ARP table
Returns a regular standard MAC address
"""
cisco_hardware_addr = cisco_hardware_addr.replace('.', '')
blocks = [cisco_hardware_addr[x:x + 2]
for x in range(0, len(cisco_hardware_addr), 2)]
return ':'.join(blocks).upper()

View File

@@ -8,7 +8,9 @@ import asyncio
from functools import partial
import logging
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE,
STATE_NOT_HOME,
HTTP_UNPROCESSABLE_ENTITY)
from homeassistant.components.http import HomeAssistantView
# pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA
@@ -76,11 +78,13 @@ class LocativeView(HomeAssistantView):
device = data['device'].replace('-', '')
location_name = data['id'].lower()
direction = data['trigger']
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
if direction == 'enter':
yield from self.hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
location_name=location_name))
location_name=location_name,
gps=gps_location))
return 'Setting location to {}'.format(location_name)
elif direction == 'exit':
@@ -88,9 +92,11 @@ class LocativeView(HomeAssistantView):
'{}.{}'.format(DOMAIN, device))
if current_state is None or current_state.state == location_name:
location_name = STATE_NOT_HOME
yield from self.hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
location_name=STATE_NOT_HOME))
location_name=location_name,
gps=gps_location))
return 'Setting location to not home'
else:
# Ignore the message if it is telling us to exit a zone that we

View File

@@ -2,7 +2,7 @@
Support for scanning a network with nmap.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.nmap_scanner/
https://home-assistant.io/components/device_tracker.nmap_tracker/
"""
import logging
import re
@@ -43,6 +43,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])

View File

@@ -0,0 +1,108 @@
"""
Support for Swisscom routers (Internet-Box).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.swisscom/
"""
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
DEFAULT_IP = '192.168.1.1'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string
})
def get_scanner(hass, config):
"""Return the Swisscom device scanner."""
scanner = SwisscomDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class SwisscomDeviceScanner(object):
"""This class queries a router running Swisscom Internet-Box firmware."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible.
data = self.get_swisscom_data()
self.success_init = data is not None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [client['mac'] for client in self.last_results]
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
if not self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['host']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the Swisscom router is up to date.
Return boolean if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Loading data from Swisscom Internet Box")
data = self.get_swisscom_data()
if not data:
return False
active_clients = [client for client in data.values() if
client['status']]
self.last_results = active_clients
return True
def get_swisscom_data(self):
"""Retrieve data from Swisscom and return parsed result."""
url = 'http://{}/ws'.format(self.host)
headers = {'Content-Type': 'application/x-sah-ws-4-call+json'}
data = """
{"service":"Devices", "method":"get",
"parameters":{"expression":"lan and not self"}}"""
request = requests.post(url, headers=headers, data=data, timeout=10)
devices = {}
for device in request.json()['status']:
try:
devices[device['Key']] = {
'ip': device['IPAddress'],
'mac': device['PhysAddress'],
'host': device['Name'],
'status': device['Active']
}
except (KeyError, requests.exceptions.RequestException):
pass
return devices

View File

@@ -55,25 +55,30 @@ def setup_scanner(hass, config, see):
"""True if any door/window is opened."""
return any([door[key] for key in door if "Open" in key])
attributes = dict(
unlocked=not vehicle["carLocked"],
tank_volume=vehicle["fuelTankVolume"],
average_fuel_consumption=round(
vehicle["averageFuelConsumption"] / 10, 1), # l/100km
washer_fluid_low=vehicle["washerFluidLevel"] != "Normal",
brake_fluid_low=vehicle["brakeFluid"] != "Normal",
service_warning=vehicle["serviceWarningStatus"] != "Normal",
bulb_failures=len(vehicle["bulbFailures"]) > 0,
doors_open=any_opened(vehicle["doors"]),
windows_open=any_opened(vehicle["windows"]),
fuel=vehicle["fuelAmount"],
odometer=round(vehicle["odometer"] / 1000), # km
range=vehicle["distanceToEmpty"])
if "heater" in vehicle and \
"status" in vehicle["heater"]:
attributes.update(heater_on=vehicle["heater"]["status"] != "off")
see(dev_id=dev_id,
host_name=host_name,
gps=(position["latitude"],
position["longitude"]),
attributes=dict(
unlocked=not vehicle["carLocked"],
tank_volume=vehicle["fuelTankVolume"],
average_fuel_consumption=round(
vehicle["averageFuelConsumption"] / 10, 1), # l/100km
washer_fluid_low=vehicle["washerFluidLevel"] != "Normal",
brake_fluid_low=vehicle["brakeFluid"] != "Normal",
service_warning=vehicle["serviceWarningStatus"] != "Normal",
bulb_failures=len(vehicle["bulbFailures"]) > 0,
doors_open=any_opened(vehicle["doors"]),
windows_open=any_opened(vehicle["windows"]),
heater_on=vehicle["heater"]["status"] != "off",
fuel=vehicle["fuelAmount"],
odometer=round(vehicle["odometer"] / 1000), # km
range=vehicle["distanceToEmpty"]))
attributes=attributes)
def update(now):
"""Update status from the online service."""

View File

@@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.10.0']
REQUIREMENTS = ['python-digitalocean==1.10.1']
_LOGGER = logging.getLogger(__name__)

View File

@@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover
REQUIREMENTS = ['netdisco==0.7.5']
REQUIREMENTS = ['netdisco==0.7.6']
DOMAIN = 'discovery'

View File

@@ -18,7 +18,7 @@ from homeassistant import util, core
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
@@ -76,6 +76,7 @@ def setup(hass, yaml_config):
ssl_certificate=None,
ssl_key=None,
cors_origins=[],
use_x_forwarded_for=False,
trusted_networks=[]
)
@@ -317,7 +318,16 @@ class HueLightsView(HomeAssistantView):
# Construct what we need to send to the service
data = {ATTR_ENTITY_ID: entity_id}
if brightness is not None:
# If the requested entity is a script add some variables
if entity.domain.lower() == "script":
data['variables'] = {
'requested_state': STATE_ON if result else STATE_OFF
}
if brightness is not None:
data['variables']['requested_level'] = brightness
elif brightness is not None:
data[ATTR_BRIGHTNESS] = brightness
if entity.domain.lower() in config.off_maps_to_on_domains:
@@ -401,6 +411,13 @@ def parse_hue_api_put_light_body(request_json, entity):
report_brightness = True
result = (brightness > 0)
elif entity.domain.lower() == "script":
# Convert 0-255 to 0-100
level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
brightness = round(level)
report_brightness = True
result = True
return (result, brightness) if report_brightness else (result, None)

View File

@@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
from homeassistant.components.discovery import load_platform
REQUIREMENTS = ['pyenvisalink==1.7', 'pydispatcher==2.0.5']
REQUIREMENTS = ['pyenvisalink==1.9', 'pydispatcher==2.0.5']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'envisalink'

View File

@@ -0,0 +1,292 @@
"""
Support for Google - Calendar Event Devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/google/
NOTE TO OTHER DEVELOPERS: IF YOU ADD MORE SCOPES TO THE OAUTH THAN JUST
CALENDAR THEN USERS WILL NEED TO DELETE THEIR TOKEN_FILE. THEY WILL LOSE THEIR
REFRESH_TOKEN PIECE WHEN RE-AUTHENTICATING TO ADD MORE API ACCESS
IT'S BEST TO JUST HAVE SEPARATE OAUTH FOR DIFFERENT PIECES OF GOOGLE
"""
import logging
import os
import yaml
import voluptuous as vol
from voluptuous.error import Error as VoluptuousError
import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
from homeassistant import bootstrap
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import track_time_change
from homeassistant.util import convert, dt
REQUIREMENTS = [
'google-api-python-client==1.5.5',
'oauth2client==3.0.0',
]
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'google'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_TRACK_NEW = 'track_new_calendar'
CONF_CAL_ID = 'cal_id'
CONF_DEVICE_ID = 'device_id'
CONF_NAME = 'name'
CONF_ENTITIES = 'entities'
CONF_TRACK = 'track'
CONF_SEARCH = 'search'
CONF_OFFSET = 'offset'
DEFAULT_CONF_TRACK_NEW = True
DEFAULT_CONF_OFFSET = '!!'
NOTIFICATION_ID = 'google_calendar_notification'
NOTIFICATION_TITLE = 'Google Calendar Setup'
GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"
SERVICE_SCAN_CALENDARS = 'scan_for_calendars'
SERVICE_FOUND_CALENDARS = 'found_calendar'
DATA_INDEX = 'google_calendars'
YAML_DEVICES = '{}_calendars.yaml'.format(DOMAIN)
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
TOKEN_FILE = '.{}.token'.format(DOMAIN)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_TRACK_NEW): cv.boolean,
})
}, extra=vol.ALLOW_EXTRA)
_SINGLE_CALSEARCH_CONFIG = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_TRACK): cv.boolean,
vol.Optional(CONF_SEARCH): vol.Any(cv.string, None),
vol.Optional(CONF_OFFSET): cv.string,
})
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_CAL_ID): cv.string,
vol.Required(CONF_ENTITIES, None):
vol.All(cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]),
}, extra=vol.ALLOW_EXTRA)
def do_authentication(hass, config):
"""Notify user of actions and authenticate.
Notify user of user_code and verification_url then poll
until we have an access token.
"""
from oauth2client.client import (
OAuth2WebServerFlow,
OAuth2DeviceCodeError,
FlowExchangeError
)
from oauth2client.file import Storage
oauth = OAuth2WebServerFlow(
config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET],
'https://www.googleapis.com/auth/calendar.readonly',
'Home-Assistant.io',
)
persistent_notification = loader.get_component('persistent_notification')
try:
dev_flow = oauth.step1_get_device_and_user_codes()
except OAuth2DeviceCodeError as err:
persistent_notification.create(
hass, 'Error: {}<br />You will need to restart hass after fixing.'
''.format(err),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
persistent_notification.create(
hass, 'In order to authorize Home-Assistant to view your calendars'
'You must visit: <a href="{}" target="_blank">{}</a> and enter'
'code: {}'.format(dev_flow.verification_url,
dev_flow.verification_url,
dev_flow.user_code),
title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID
)
def step2_exchange(now):
"""Keep trying to validate the user_code until it expires."""
if now >= dt.as_local(dev_flow.user_code_expiry):
persistent_notification.create(
hass, 'Authenication code expired, please restart '
'Home-Assistant and try again',
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
listener()
try:
credentials = oauth.step2_exchange(device_flow_info=dev_flow)
except FlowExchangeError:
# not ready yet, call again
return
storage = Storage(hass.config.path(TOKEN_FILE))
storage.put(credentials)
do_setup(hass, config)
listener()
persistent_notification.create(
hass, 'We are all setup now. Check {} for calendars that have '
'been found'.format(YAML_DEVICES),
title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID)
listener = track_time_change(hass, step2_exchange,
second=range(0, 60, dev_flow.interval))
return True
def setup(hass, config):
"""Setup the platform."""
if DATA_INDEX not in hass.data:
hass.data[DATA_INDEX] = {}
conf = config.get(DOMAIN, {})
token_file = hass.config.path(TOKEN_FILE)
if not os.path.isfile(token_file):
do_authentication(hass, conf)
else:
do_setup(hass, conf)
return True
def setup_services(hass, track_new_found_calendars, calendar_service):
"""Setup service listeners."""
def _found_calendar(call):
"""Check if we know about a calendar and generate PLATFORM_DISCOVER."""
calendar = get_calendar_info(hass, call.data)
if hass.data[DATA_INDEX].get(calendar[CONF_CAL_ID], None) is not None:
return
hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar})
update_config(
hass.config.path(YAML_DEVICES),
hass.data[DATA_INDEX][calendar[CONF_CAL_ID]]
)
discovery.load_platform(hass, 'calendar', DOMAIN,
hass.data[DATA_INDEX][calendar[CONF_CAL_ID]])
hass.services.register(
DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar,
None, schema=None)
def _scan_for_calendars(service):
"""Scan for new calendars."""
service = calendar_service.get()
cal_list = service.calendarList() # pylint: disable=no-member
calendars = cal_list.list().execute()['items']
for calendar in calendars:
calendar['track'] = track_new_found_calendars
hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS,
calendar)
hass.services.register(
DOMAIN, SERVICE_SCAN_CALENDARS,
_scan_for_calendars,
None, schema=None)
return True
def do_setup(hass, config):
"""Run the setup after we have everything configured."""
# load calendars the user has configured
hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES))
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
track_new_found_calendars = convert(config.get(CONF_TRACK_NEW),
bool, DEFAULT_CONF_TRACK_NEW)
setup_services(hass, track_new_found_calendars, calendar_service)
# Ensure component is loaded
bootstrap.setup_component(hass, 'calendar', config)
for calendar in hass.data[DATA_INDEX].values():
discovery.load_platform(hass, 'calendar', DOMAIN, calendar)
# look for any new calendars
hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None)
return True
class GoogleCalendarService(object):
"""Calendar service interface to google."""
def __init__(self, token_file):
"""We just need the token_file."""
self.token_file = token_file
def get(self):
"""Get the calendar service from the storage file token."""
import httplib2
from oauth2client.file import Storage
from googleapiclient import discovery as google_discovery
credentials = Storage(self.token_file).get()
http = credentials.authorize(httplib2.Http())
service = google_discovery.build('calendar', 'v3', http=http)
return service
def get_calendar_info(hass, calendar):
"""Convert data from Google into DEVICE_SCHEMA."""
calendar_info = DEVICE_SCHEMA({
CONF_CAL_ID: calendar['id'],
CONF_ENTITIES: [{
CONF_TRACK: calendar['track'],
CONF_NAME: calendar['summary'],
CONF_DEVICE_ID: generate_entity_id('{}', calendar['summary'],
hass=hass),
}]
})
return calendar_info
def load_config(path):
"""Load the google_calendar_devices.yaml."""
calendars = {}
try:
with open(path) as file:
data = yaml.load(file)
for calendar in data:
try:
calendars.update({calendar[CONF_CAL_ID]:
DEVICE_SCHEMA(calendar)})
except VoluptuousError as exception:
# keep going
_LOGGER.warning('Calendar Invalid Data: %s', exception)
except FileNotFoundError:
# When YAML file could not be loaded/did not contain a dict
return {}
return calendars
def update_config(path, calendar):
"""Write the google_calendar_devices.yaml."""
with open(path, 'a') as out:
out.write('\n')
yaml.dump([calendar], out, default_flow_style=False)

View File

@@ -184,7 +184,7 @@ def async_setup(hass, config):
tasks = [group.async_set_visible(visible) for group
in component.async_extract_from_service(service,
expand_group=False)]
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
@@ -207,13 +207,14 @@ def _async_process_config(hass, config, component):
icon = conf.get(CONF_ICON)
view = conf.get(CONF_VIEW)
# This order is important as groups get a number based on creation
# order.
# Don't create tasks and await them all. The order is important as
# groups get a number based on creation order.
group = yield from Group.async_create_group(
hass, name, entity_ids, icon=icon, view=view, object_id=object_id)
groups.append(group)
yield from component.async_add_entities(groups)
if groups:
yield from component.async_add_entities(groups)
class Group(Entity):
@@ -394,7 +395,7 @@ class Group(Entity):
This method must be run in the event loop.
"""
self._async_update_group_state(new_state)
self.hass.loop.create_task(self.async_update_ha_state())
self.hass.async_add_job(self.async_update_ha_state())
@property
def _tracking_states(self):

View File

@@ -28,7 +28,7 @@ from homeassistant import util
from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR)
import homeassistant.helpers.config_validation as cv
from homeassistant.components import persistent_notification
@@ -42,6 +42,7 @@ CONF_DEVELOPMENT = 'development'
CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_KEY = 'ssl_key'
CONF_CORS_ORIGINS = 'cors_allowed_origins'
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
CONF_TRUSTED_NETWORKS = 'trusted_networks'
DATA_API_PASSWORD = 'api_password'
@@ -82,6 +83,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
vol.Optional(CONF_TRUSTED_NETWORKS):
vol.All(cv.ensure_list, [ip_network])
}),
@@ -125,6 +127,7 @@ def setup(hass, config):
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
trusted_networks = [
ip_network(trusted_network)
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
@@ -138,6 +141,7 @@ def setup(hass, config):
ssl_certificate=ssl_certificate,
ssl_key=ssl_key,
cors_origins=cors_origins,
use_x_forwarded_for=use_x_forwarded_for,
trusted_networks=trusted_networks
)
@@ -218,6 +222,7 @@ class GzipFileSender(FileSender):
return resp
_GZIP_FILE_SENDER = GzipFileSender()
@@ -248,7 +253,7 @@ class HomeAssistantWSGI(object):
def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins,
trusted_networks):
use_x_forwarded_for, trusted_networks):
"""Initialize the WSGI Home Assistant server."""
import aiohttp_cors
@@ -260,6 +265,7 @@ class HomeAssistantWSGI(object):
self.ssl_key = ssl_key
self.server_host = server_host
self.server_port = server_port
self.use_x_forwarded_for = use_x_forwarded_for
self.trusted_networks = trusted_networks
self.event_forwarder = None
self._handler = None
@@ -366,11 +372,15 @@ class HomeAssistantWSGI(object):
yield from self._handler.finish_connections(60.0)
yield from self.app.cleanup()
@staticmethod
def get_real_ip(request):
def get_real_ip(self, request):
"""Return the clients correct ip address, even in proxied setups."""
peername = request.transport.get_extra_info('peername')
return peername[0] if peername is not None else None
if self.use_x_forwarded_for \
and HTTP_HEADER_X_FORWARDED_FOR in request.headers:
return request.headers.get(
HTTP_HEADER_X_FORWARDED_FOR).split(',')[0]
else:
peername = request.transport.get_extra_info('peername')
return peername[0] if peername is not None else None
def is_trusted_ip(self, remote_addr):
"""Match an ip address against trusted CIDR networks."""
@@ -452,7 +462,10 @@ def request_handler_factory(view, handler):
@asyncio.coroutine
def handle(request):
"""Handle incoming request."""
remote_addr = HomeAssistantWSGI.get_real_ip(request)
if not view.hass.is_running:
return web.Response(status=503)
remote_addr = view.hass.http.get_real_ip(request)
# Auth code verbose on purpose
authenticated = False

View File

@@ -23,17 +23,23 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
_LOGGER = logging.getLogger(__name__)
CONF_INITIAL = 'initial'
DEFAULT_INITIAL = False
SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
cv.slug: vol.Any({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_INITIAL, default=False): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
}, None)}}, extra=vol.ALLOW_EXTRA)
DEFAULT_CONFIG = {CONF_INITIAL: DEFAULT_INITIAL}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: vol.Any({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
}, None)
})
}, extra=vol.ALLOW_EXTRA)
def is_on(hass, entity_id):
@@ -65,10 +71,10 @@ def async_setup(hass, config):
for object_id, cfg in config[DOMAIN].items():
if not cfg:
cfg = {}
cfg = DEFAULT_CONFIG
name = cfg.get(CONF_NAME)
state = cfg.get(CONF_INITIAL, False)
state = cfg.get(CONF_INITIAL)
icon = cfg.get(CONF_ICON)
entities.append(InputBoolean(object_id, name, state, icon))
@@ -89,7 +95,7 @@ def async_setup(hass, config):
attr = 'async_toggle'
tasks = [getattr(input_b, attr)() for input_b in target_inputs]
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA)

View File

@@ -55,14 +55,16 @@ def _cv_input_select(cfg):
return cfg
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1),
[cv.string]),
vol.Optional(CONF_INITIAL): cv.string,
vol.Optional(CONF_ICON): cv.icon,
}, _cv_input_select)}}, required=True, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_OPTIONS):
vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]),
vol.Optional(CONF_INITIAL): cv.string,
vol.Optional(CONF_ICON): cv.icon,
}, _cv_input_select)})
}, required=True, extra=vol.ALLOW_EXTRA)
def select_option(hass, entity_id, option):
@@ -111,7 +113,7 @@ def async_setup(hass, config):
tasks = [input_select.async_select_option(call.data[ATTR_OPTION])
for input_select in target_inputs]
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service,
@@ -124,7 +126,7 @@ def async_setup(hass, config):
tasks = [input_select.async_offset_index(1)
for input_select in target_inputs]
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service,
@@ -137,7 +139,7 @@ def async_setup(hass, config):
tasks = [input_select.async_offset_index(-1)
for input_select in target_inputs]
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service,

View File

@@ -51,17 +51,21 @@ def _cv_input_slider(cfg):
cfg[CONF_INITIAL] = state
return cfg
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_MIN): vol.Coerce(float),
vol.Required(CONF_MAX): vol.Coerce(float),
vol.Optional(CONF_INITIAL): vol.Coerce(float),
vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float),
vol.Range(min=1e-3)),
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string
}, _cv_input_slider)}}, required=True, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_MIN): vol.Coerce(float),
vol.Required(CONF_MAX): vol.Coerce(float),
vol.Optional(CONF_INITIAL): vol.Coerce(float),
vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float),
vol.Range(min=1e-3)),
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string
}, _cv_input_slider)
})
}, required=True, extra=vol.ALLOW_EXTRA)
def select_value(hass, entity_id, value):
@@ -101,7 +105,7 @@ def async_setup(hass, config):
tasks = [input_slider.async_select_value(call.data[ATTR_VALUE])
for input_slider in target_inputs]
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service,

View File

@@ -2,7 +2,7 @@
Native Home Assistant iOS app component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/ios/
https://home-assistant.io/ecosystem/ios/
"""
import asyncio
import os

View File

@@ -161,8 +161,6 @@ class KNXGroupAddress(Entity):
@property
def is_on(self):
"""Return True if the value is not 0 is on, else False."""
if self.should_poll:
self.update()
return self._state != 0
@property

View File

@@ -10,6 +10,7 @@ import csv
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import group
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
@@ -20,6 +21,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
from homeassistant.util.async import run_callback_threadsafe
DOMAIN = "light"
@@ -128,6 +130,18 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None):
"""Turn all or specified light on."""
run_callback_threadsafe(
hass.loop, async_turn_on, hass, entity_id, transition, brightness,
rgb_color, xy_color, color_temp, white_value,
profile, flash, effect, color_name).result()
@callback
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None,
white_value=None, profile=None, flash=None, effect=None,
color_name=None):
"""Turn all or specified light on."""
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
@@ -144,10 +158,17 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id=None, transition=None):
"""Turn all or specified light off."""
run_callback_threadsafe(
hass.loop, async_turn_off, hass, entity_id, transition).result()
@callback
def async_turn_off(hass, entity_id=None, transition=None):
"""Turn all or specified light off."""
data = {
key: value for key, value in [
@@ -156,7 +177,8 @@ def turn_off(hass, entity_id=None, transition=None):
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_OFF,
data)
def toggle(hass, entity_id=None, transition=None):

View File

@@ -17,12 +17,13 @@ from homeassistant.components.light import (
PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.8.zip'
'#flux_led==0.8']
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.9.zip'
'#flux_led==0.9']
_LOGGER = logging.getLogger(__name__)
CONF_AUTOMATIC_ADD = 'automatic_add'
ATTR_MODE = 'mode'
DOMAIN = 'flux_led'
@@ -31,6 +32,8 @@ SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(ATTR_MODE, default='rgbw'):
vol.All(cv.string, vol.In(['rgbw', 'rgb'])),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -48,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device = {}
device['name'] = device_config[CONF_NAME]
device['ipaddr'] = ipaddr
device[ATTR_MODE] = device_config[ATTR_MODE]
light = FluxLight(device)
if light.is_valid:
lights.append(light)
@@ -65,6 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if ipaddr in light_ips:
continue
device['name'] = device['id'] + " " + ipaddr
device[ATTR_MODE] = 'rgbw'
light = FluxLight(device)
if light.is_valid:
lights.append(light)
@@ -82,6 +87,7 @@ class FluxLight(Light):
self._name = device['name']
self._ipaddr = device['ipaddr']
self._mode = device[ATTR_MODE]
self.is_valid = True
self._bulb = None
try:
@@ -129,10 +135,16 @@ class FluxLight(Light):
rgb = kwargs.get(ATTR_RGB_COLOR)
brightness = kwargs.get(ATTR_BRIGHTNESS)
effect = kwargs.get(ATTR_EFFECT)
if rgb:
if rgb is not None and brightness is not None:
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
elif rgb is not None:
self._bulb.setRgb(*tuple(rgb))
elif brightness:
self._bulb.setWarmWhite255(brightness)
elif brightness is not None:
if self._mode == 'rgbw':
self._bulb.setWarmWhite255(brightness)
elif self._mode == 'rgb':
(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),

View File

@@ -22,11 +22,12 @@ from homeassistant.components.light import (
FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME)
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['phue==0.8']
REQUIREMENTS = ['phue==0.9']
# Track previously setup bridges
_CONFIGURED_BRIDGES = {}
@@ -37,6 +38,8 @@ _LOGGER = logging.getLogger(__name__)
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
DEFAULT_ALLOW_UNREACHABLE = False
DOMAIN = "light"
SERVICE_HUE_SCENE = "hue_activate_scene"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
@@ -53,6 +56,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FILENAME): cv.string,
})
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
})
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
"""Attempt to detect host based on existing configuration."""
@@ -166,6 +176,21 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
add_devices(new_lights)
_CONFIGURED_BRIDGES[socket.gethostbyname(host)] = True
# create a service for calling run_scene directly on the bridge,
# used to simplify automation rules.
def hue_activate_scene(call):
"""Service to call directly directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
bridge.run_scene(group_name, scene_name)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
descriptions.get(SERVICE_HUE_SCENE),
schema=SCENE_SCHEMA)
update_lights()

View File

@@ -47,7 +47,7 @@ class LiteJetLight(Light):
def _on_load_changed(self):
"""Called on a LiteJet thread when a load's state changes."""
_LOGGER.debug("Updating due to notification for %s", self._name)
self._hass.loop.create_task(self.async_update_ha_state(True))
self._hass.async_add_job(self.async_update_ha_state(True))
@property
def name(self):

View File

@@ -0,0 +1,252 @@
"""
Support for MQTT Template lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.mqtt_template/
"""
import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA,
ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'mqtt_template'
DEPENDENCIES = ['mqtt']
DEFAULT_NAME = 'MQTT Template Light'
DEFAULT_OPTIMISTIC = False
CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
CONF_STATE_TEMPLATE = 'state_template'
CONF_BRIGHTNESS_TEMPLATE = 'brightness_template'
CONF_RED_TEMPLATE = 'red_template'
CONF_GREEN_TEMPLATE = 'green_template'
CONF_BLUE_TEMPLATE = 'blue_template'
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH |
SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template,
vol.Optional(CONF_RED_TEMPLATE): cv.template,
vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a MQTT Template light."""
add_devices([MqttTemplate(
hass,
config.get(CONF_NAME),
{
key: config.get(key) for key in (
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC
)
},
{
key: config.get(key) for key in (
CONF_COMMAND_ON_TEMPLATE,
CONF_COMMAND_OFF_TEMPLATE,
CONF_STATE_TEMPLATE,
CONF_BRIGHTNESS_TEMPLATE,
CONF_RED_TEMPLATE,
CONF_GREEN_TEMPLATE,
CONF_BLUE_TEMPLATE
)
},
config.get(CONF_OPTIMISTIC),
config.get(CONF_QOS),
config.get(CONF_RETAIN)
)])
class MqttTemplate(Light):
"""Representation of a MQTT Template light."""
def __init__(self, hass, name, topics, templates, optimistic, qos, retain):
"""Initialize MQTT Template light."""
self._hass = hass
self._name = name
self._topics = topics
self._templates = templates
for tpl in self._templates.values():
if tpl is not None:
tpl.hass = hass
self._optimistic = optimistic or topics[CONF_STATE_TOPIC] is None \
or templates[CONF_STATE_TEMPLATE] is None
self._qos = qos
self._retain = retain
# features
self._state = False
if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None:
self._brightness = 255
else:
self._brightness = None
if (self._templates[CONF_RED_TEMPLATE] is not None and
self._templates[CONF_GREEN_TEMPLATE] is not None and
self._templates[CONF_BLUE_TEMPLATE] is not None):
self._rgb = [0, 0, 0]
else:
self._rgb = None
def state_received(topic, payload, qos):
"""A new MQTT message has been received."""
# read state
state = self._templates[CONF_STATE_TEMPLATE].\
render_with_possible_json_value(payload)
if state == STATE_ON:
self._state = True
elif state == STATE_OFF:
self._state = False
else:
_LOGGER.warning('Invalid state value received')
# read brightness
if self._brightness is not None:
try:
self._brightness = int(
self._templates[CONF_BRIGHTNESS_TEMPLATE].
render_with_possible_json_value(payload)
)
except ValueError:
_LOGGER.warning('Invalid brightness value received')
# read color
if self._rgb is not None:
try:
self._rgb[0] = int(
self._templates[CONF_RED_TEMPLATE].
render_with_possible_json_value(payload))
self._rgb[1] = int(
self._templates[CONF_GREEN_TEMPLATE].
render_with_possible_json_value(payload))
self._rgb[2] = int(
self._templates[CONF_BLUE_TEMPLATE].
render_with_possible_json_value(payload))
except ValueError:
_LOGGER.warning('Invalid color value received')
self.update_ha_state()
if self._topics[CONF_STATE_TOPIC] is not None:
mqtt.subscribe(self._hass, self._topics[CONF_STATE_TOPIC],
state_received, self._qos)
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
def rgb_color(self):
"""Return the RGB color value [int, int, int]."""
return self._rgb
@property
def should_poll(self):
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return False
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def is_on(self):
"""Return True if entity is on."""
return self._state
@property
def assumed_state(self):
"""Return True if unable to access real state of the entity."""
return self._optimistic
def turn_on(self, **kwargs):
"""Turn the entity on."""
# state
values = {'state': True}
if self._optimistic:
self._state = True
# brightness
if ATTR_BRIGHTNESS in kwargs:
values['brightness'] = int(kwargs[ATTR_BRIGHTNESS])
if self._optimistic:
self._brightness = kwargs[ATTR_BRIGHTNESS]
# color
if ATTR_RGB_COLOR in kwargs:
values['red'] = kwargs[ATTR_RGB_COLOR][0]
values['green'] = kwargs[ATTR_RGB_COLOR][1]
values['blue'] = kwargs[ATTR_RGB_COLOR][2]
if self._optimistic:
self._rgb = kwargs[ATTR_RGB_COLOR]
# flash
if ATTR_FLASH in kwargs:
values['flash'] = kwargs.get(ATTR_FLASH)
# transition
if ATTR_TRANSITION in kwargs:
values['transition'] = kwargs[ATTR_TRANSITION]
mqtt.publish(
self._hass, self._topics[CONF_COMMAND_TOPIC],
self._templates[CONF_COMMAND_ON_TEMPLATE].render(**values),
self._qos, self._retain
)
if self._optimistic:
self.update_ha_state()
def turn_off(self, **kwargs):
"""Turn the entity off."""
# state
values = {'state': False}
if self._optimistic:
self._state = False
# transition
if ATTR_TRANSITION in kwargs:
values['transition'] = kwargs[ATTR_TRANSITION]
mqtt.publish(
self._hass, self._topics[CONF_COMMAND_TOPIC],
self._templates[CONF_COMMAND_OFF_TEMPLATE].render(**values),
self._qos, self._retain
)
if self._optimistic:
self.update_ha_state()

View File

@@ -31,7 +31,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None:
return
for gateway in mysensors.GATEWAYS.values():
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
if not gateways:
return
for gateway in gateways:
# Define the S_TYPES and V_TYPES that the platform should handle as
# states. Map them in a dict of lists.
pres = gateway.const.Presentation

View File

@@ -81,3 +81,15 @@ toggle:
transition:
description: Duration in seconds it takes to get to next state
example: 60
hue_activate_scene:
description: Activate a hue scene stored in the hue hub
fields:
group_name:
description: Name of hue group/room from the hue app
example: "Living Room"
scene_name:
description: Name of hue scene from the hue app
example: "Energize"

View File

@@ -41,7 +41,10 @@ class WinkLight(WinkDevice, Light):
@property
def brightness(self):
"""Return the brightness of the light."""
return int(self.wink.brightness() * 255)
if self.wink.brightness() is not None:
return int(self.wink.brightness() * 255)
else:
return None
@property
def rgb_color(self):
@@ -52,6 +55,8 @@ class WinkLight(WinkDevice, Light):
hue = self.wink.color_hue()
saturation = self.wink.color_saturation()
value = int(self.wink.brightness() * 255)
if hue is None or saturation is None or value is None:
return None
rgb = colorsys.hsv_to_rgb(hue, saturation, value)
r_value = int(round(rgb[0]))
g_value = int(round(rgb[1]))

View File

@@ -24,26 +24,6 @@ AEOTEC = 0x86
AEOTEC_ZW098_LED_BULB = 0x62
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
LINEAR = 0x14f
LINEAR_WD500Z_DIMMER = 0x3034
LINEAR_WD500Z_DIMMER_LIGHT = (LINEAR, LINEAR_WD500Z_DIMMER)
GE = 0x63
GE_12724_DIMMER = 0x3031
GE_12724_DIMMER_LIGHT = (GE, GE_12724_DIMMER)
DRAGONTECH = 0x184
DRAGONTECH_PD100_DIMMER = 0x3032
DRAGONTECH_PD100_DIMMER_LIGHT = (DRAGONTECH, DRAGONTECH_PD100_DIMMER)
ACT = 0x01
ACT_ZDP100_DIMMER = 0x3030
ACT_ZDP100_DIMMER_LIGHT = (ACT, ACT_ZDP100_DIMMER)
HOMESEER = 0x0c
HOMESEER_WD100_DIMMER = 0x3034
HOMESEER_WD100_DIMMER_LIGHT = (HOMESEER, HOMESEER_WD100_DIMMER)
COLOR_CHANNEL_WARM_WHITE = 0x01
COLOR_CHANNEL_COLD_WHITE = 0x02
COLOR_CHANNEL_RED = 0x04
@@ -51,15 +31,9 @@ COLOR_CHANNEL_GREEN = 0x08
COLOR_CHANNEL_BLUE = 0x10
WORKAROUND_ZW098 = 'zw098'
WORKAROUND_DELAY = 'alt_delay'
DEVICE_MAPPINGS = {
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098,
LINEAR_WD500Z_DIMMER_LIGHT: WORKAROUND_DELAY,
GE_12724_DIMMER_LIGHT: WORKAROUND_DELAY,
DRAGONTECH_PD100_DIMMER_LIGHT: WORKAROUND_DELAY,
ACT_ZDP100_DIMMER_LIGHT: WORKAROUND_DELAY,
HOMESEER_WD100_DIMMER_LIGHT: WORKAROUND_DELAY,
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
}
# Generate midpoint color temperatures for bulbs that have limited
@@ -75,10 +49,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and add Z-Wave lights."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
customize = hass.data['zwave_customize']
name = super().entity_id
node_config = customize.get(name, {})
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
_LOGGER.debug('customize=%s name=%s node_config=%s CONF_REFRESH_VALUE=%s'
' CONF_REFRESH_DELAY=%s', customize, name, node_config,
refresh, delay)
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
return
if value.type != zwave.const.TYPE_BYTE:
@@ -89,9 +69,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
value.set_change_verified(False)
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
add_devices([ZwaveColorLight(value)])
add_devices([ZwaveColorLight(value, refresh, delay)])
else:
add_devices([ZwaveDimmer(value)])
add_devices([ZwaveDimmer(value, refresh, delay)])
def brightness_state(value):
@@ -105,7 +85,7 @@ def brightness_state(value):
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
"""Representation of a Z-Wave dimmer."""
def __init__(self, value):
def __init__(self, value, refresh, delay):
"""Initialize the light."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
@@ -113,7 +93,8 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._brightness = None
self._state = None
self._alt_delay = None
self._delay = delay
self._refresh_value = refresh
self._zw098 = None
# Enable appropriate workaround flags for our device
@@ -126,17 +107,14 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
self._zw098 = 1
elif DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_DELAY:
_LOGGER.debug("Dimmer delay workaround enabled for node:"
" %s", value.parent_id)
self._alt_delay = 1
self.update_properties()
# Used for value change event handling
self._refreshing = False
self._timer = None
_LOGGER.debug('self._refreshing=%s self.delay=%s',
self._refresh_value, self._delay)
dispatcher.connect(
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
@@ -149,26 +127,25 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
if self._refreshing:
self._refreshing = False
self.update_properties()
else:
def _refresh_value():
"""Used timer callback for delayed value refresh."""
self._refreshing = True
self._value.refresh()
if self._timer is not None and self._timer.isAlive():
self._timer.cancel()
if self._alt_delay:
self._timer = Timer(5, _refresh_value)
if self._refresh_value:
if self._refreshing:
self._refreshing = False
self.update_properties()
else:
self._timer = Timer(2, _refresh_value)
self._timer.start()
def _refresh_value():
"""Used timer callback for delayed value refresh."""
self._refreshing = True
self._value.refresh()
self.update_ha_state()
if self._timer is not None and self._timer.isAlive():
self._timer.cancel()
self._timer = Timer(self._delay, _refresh_value)
self._timer.start()
self.update_ha_state()
else:
self.update_properties()
self.update_ha_state()
@property
def brightness(self):
@@ -213,7 +190,7 @@ def ct_to_rgb(temp):
class ZwaveColorLight(ZwaveDimmer):
"""Representation of a Z-Wave color changing light."""
def __init__(self, value):
def __init__(self, value, refresh, delay):
"""Initialize the light."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher

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.5.zip'
'#braviarc==0.3.5']
'https://github.com/aparraga/braviarc/archive/0.3.6.zip'
'#braviarc==0.3.6']
BRAVIA_CONFIG_FILE = 'bravia.conf'

View File

@@ -280,9 +280,9 @@ class CastDevice(MediaPlayerDevice):
def new_cast_status(self, status):
"""Called when a new cast status is received."""
self.cast_status = status
self.update_ha_state()
self.schedule_update_ha_state()
def new_media_status(self, status):
"""Called when a new media status is received."""
self.media_status = status
self.update_ha_state()
self.schedule_update_ha_state()

View File

@@ -104,7 +104,7 @@ class KodiDevice(MediaPlayerDevice):
if len(self._players) == 0:
return STATE_IDLE
if self._properties['speed'] == 0:
if self._properties['speed'] == 0 and not self._properties['live']:
return STATE_PAUSED
else:
return STATE_PLAYING
@@ -120,7 +120,7 @@ class KodiDevice(MediaPlayerDevice):
self._properties = self._server.Player.GetProperties(
player_id,
['time', 'totaltime', 'speed']
['time', 'totaltime', 'speed', 'live']
)
self._item = self._server.Player.GetItem(
@@ -163,7 +163,7 @@ class KodiDevice(MediaPlayerDevice):
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
if self._properties is not None:
if self._properties is not None and not self._properties['live']:
total_time = self._properties['totaltime']
return (

View File

@@ -79,16 +79,16 @@ class PanasonicVieraTVDevice(MediaPlayerDevice):
self._playing = True
self._state = STATE_UNKNOWN
self._remote = remote
self._volume = 0
def update(self):
"""Retrieve the latest data."""
try:
self._muted = self._remote.get_mute()
self._volume = self._remote.get_volume() / 100
self._state = STATE_ON
except OSError:
self._state = STATE_OFF
return False
return True
def send_key(self, key):
"""Send a key to the tv and handles exceptions."""
@@ -113,13 +113,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice):
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
volume = 0
try:
volume = self._remote.get_volume() / 100
self._state = STATE_ON
except OSError:
self._state = STATE_OFF
return volume
return self._volume
@property
def is_volume_muted(self):

View File

@@ -128,6 +128,8 @@ class SamsungTVDevice(MediaPlayerDevice):
def turn_off(self):
"""Turn off media player."""
self.send_key('KEY_POWEROFF')
# Force closing of remote session to provide instant UI feedback
self.get_remote().close()
def volume_up(self):
"""Volume up the media player."""

View File

@@ -21,9 +21,7 @@ from homeassistant.const import (
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/SoCo/SoCo/archive/'
'cf8c2701165562eccbf1ecc879bf7060ceb0993e.zip#'
'SoCo==0.12']
REQUIREMENTS = ['SoCo==0.12']
_LOGGER = logging.getLogger(__name__)
@@ -75,12 +73,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
player = soco.SoCo(discovery_info)
# if device allready exists by config
if player.uid in DEVICES:
if player.uid in [x.unique_id for x in DEVICES]:
return True
if player.is_visible:
device = SonosDevice(hass, player)
add_devices([device])
add_devices([device], True)
if not DEVICES:
register_services(hass)
DEVICES.append(device)
@@ -106,7 +104,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
DEVICES = [SonosDevice(hass, p) for p in players]
add_devices(DEVICES)
add_devices(DEVICES, True)
register_services(hass)
_LOGGER.info('Added %s Sonos speakers', len(players))
return True
@@ -256,6 +254,7 @@ class SonosDevice(MediaPlayerDevice):
self.hass = hass
self.volume_increment = 5
self._unique_id = player.uid
self._player = player
self._player_volume = None
self._player_volume_muted = None
@@ -278,7 +277,8 @@ class SonosDevice(MediaPlayerDevice):
self._current_track_is_radio_stream = False
self._queue = None
self._last_avtransport_event = None
self.update()
self._is_playing_line_in = None
self._is_playing_tv = None
self.soco_snapshot = Snapshot(self._player)
@property
@@ -286,14 +286,10 @@ class SonosDevice(MediaPlayerDevice):
"""Polling needed."""
return True
def update_sonos(self, now):
"""Update state, called by track_utc_time_change."""
self.update_ha_state(True)
@property
def unique_id(self):
"""Return an unique ID."""
return self._player.uid
return self._unique_id
@property
def name(self):
@@ -395,6 +391,10 @@ class SonosDevice(MediaPlayerDevice):
self._coordinator = None
if not self._coordinator:
is_playing_tv = self._player.is_playing_tv
is_playing_line_in = self._player.is_playing_line_in
media_info = self._player.avTransport.GetMediaInfo(
[('InstanceID', 0)]
)
@@ -408,7 +408,23 @@ class SonosDevice(MediaPlayerDevice):
current_media_uri.startswith('x-sonosapi-stream:') or \
current_media_uri.startswith('x-rincon-mp3radio:')
if is_radio_stream:
if is_playing_tv or is_playing_line_in:
# playing from line-in/tv.
support_previous_track = False
support_next_track = False
support_pause = False
if is_playing_tv:
media_artist = SUPPORT_SOURCE_TV
else:
media_artist = SUPPORT_SOURCE_LINEIN
media_album_name = None
media_title = None
media_image_url = None
elif is_radio_stream:
is_radio_stream = True
media_image_url = self._format_media_image_url(
current_media_uri
@@ -507,14 +523,16 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = support_previous_track
self._support_next_track = support_next_track
self._support_pause = support_pause
self._is_playing_tv = is_playing_tv
self._is_playing_line_in = is_playing_line_in
# update state of the whole group
# pylint: disable=protected-access
for device in [x for x in DEVICES if x._coordinator == self]:
if device.entity_id:
device.update_ha_state(False)
if device.entity_id is not self.entity_id:
self.hass.add_job(device.async_update_ha_state)
if self._queue is None and self.entity_id:
if self._queue is None and self.entity_id is not None:
self._subscribe_to_player_events()
else:
self._player_volume = None
@@ -534,6 +552,8 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = False
self._support_next_track = False
self._support_pause = False
self._is_playing_tv = False
self._is_playing_line_in = False
self._last_avtransport_event = None
@@ -713,10 +733,13 @@ class SonosDevice(MediaPlayerDevice):
@property
def source(self):
"""Name of the current input source."""
if self._player.is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
if self._player.is_playing_tv:
return SUPPORT_SOURCE_TV
if self._coordinator:
return self._coordinator.source
else:
if self._is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
elif self._is_playing_tv:
return SUPPORT_SOURCE_TV
return None

View File

@@ -111,9 +111,11 @@ class LogitechMediaServer(object):
def query(self, *parameters):
"""Send request and await response from server."""
response = urllib.parse.unquote(self.get(' '.join(parameters)))
response = self.get(' '.join(parameters))
response = response.split(' ')[-1].strip()
response = urllib.parse.unquote(response)
return response.split(' ')[-1].strip()
return response
def get_player_status(self, player):
"""Get the status of a player."""

View File

@@ -18,23 +18,19 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON,
STATE_PLAYING, STATE_IDLE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['rxv==0.3.0']
REQUIREMENTS = ['rxv==0.4.0']
_LOGGER = logging.getLogger(__name__)
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | \
SUPPORT_PLAY_MEDIA
# Only supported by some sources
SUPPORT_PLAYBACK = SUPPORT_PLAY_MEDIA | SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
CONF_SOURCE_NAMES = 'source_names'
CONF_SOURCE_IGNORE = 'source_ignore'
CONF_ZONE_IGNORE = 'zone_ignore'
DEFAULT_NAME = 'Yamaha Receiver'
KNOWN = 'yamaha_known_receivers'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -50,6 +46,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Yamaha platform."""
import rxv
# keep track of configured receivers so that we don't end up
# discovering a receiver dynamically that we have static config
# for.
if hass.data.get(KNOWN, None) is None:
hass.data[KNOWN] = set()
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
@@ -62,12 +63,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
model = discovery_info[1]
ctrl_url = discovery_info[2]
desc_url = discovery_info[3]
if ctrl_url in hass.data[KNOWN]:
_LOGGER.info("%s already manually configured", ctrl_url)
return
receivers = rxv.RXV(
ctrl_url,
model_name=model,
friendly_name=name,
unit_desc_url=desc_url).zone_controllers()
_LOGGER.info("Receivers: %s", receivers)
# when we are dynamically discovered config is empty
zone_ignore = []
elif host is None:
receivers = []
for recv in rxv.find():
@@ -78,6 +84,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for receiver in receivers:
if receiver.zone not in zone_ignore:
hass.data[KNOWN].add(receiver.ctrl_url)
add_devices([
YamahaDevice(name, receiver, source_ignore, source_names)])
@@ -175,8 +182,16 @@ class YamahaDevice(MediaPlayerDevice):
def supported_media_commands(self):
"""Flag of media commands that are supported."""
supported_commands = SUPPORT_YAMAHA
if self._is_playback_supported:
supported_commands |= SUPPORT_PLAYBACK
supports = self._receiver.get_playback_support()
mapping = {'play': SUPPORT_PLAY_MEDIA,
'pause': SUPPORT_PAUSE,
'stop': SUPPORT_STOP,
'skip_f': SUPPORT_NEXT_TRACK,
'skip_r': SUPPORT_PREVIOUS_TRACK}
for attr, feature in mapping.items():
if getattr(supports, attr, False):
supported_commands |= feature
return supported_commands
def turn_off(self):

View File

@@ -18,11 +18,12 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import template, config_validation as cv
from homeassistant.helpers.event import threaded_listener_factory
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE)
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE,
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "mqtt"
DOMAIN = 'mqtt'
MQTT_CLIENT = None
@@ -33,16 +34,15 @@ REQUIREMENTS = ['paho-mqtt==1.2']
CONF_EMBEDDED = 'embedded'
CONF_BROKER = 'broker'
CONF_PORT = 'port'
CONF_CLIENT_ID = 'client_id'
CONF_KEEPALIVE = 'keepalive'
CONF_USERNAME = 'username'
CONF_PASSWORD = 'password'
CONF_CERTIFICATE = 'certificate'
CONF_CLIENT_KEY = 'client_key'
CONF_CLIENT_CERT = 'client_cert'
CONF_TLS_INSECURE = 'tls_insecure'
CONF_PROTOCOL = 'protocol'
CONF_BIRTH_MESSAGE = 'birth_message'
CONF_WILL_MESSAGE = 'will_message'
CONF_STATE_TOPIC = 'state_topic'
CONF_COMMAND_TOPIC = 'command_topic'
@@ -78,20 +78,27 @@ def valid_publish_topic(value):
"""Validate that we can publish using this MQTT topic."""
return valid_subscribe_topic(value, invalid_chars='#+\0')
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict)
CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \
'the mqtt broker config'
MQTT_WILL_BIRTH_SCHEMA = vol.Schema({
vol.Required(ATTR_TOPIC): valid_publish_topic,
vol.Required(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string,
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
}, required=True)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE):
vol.All(vol.Coerce(int), vol.Range(min=15)),
vol.Optional(CONF_BROKER): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERTIFICATE): cv.isfile,
@@ -103,6 +110,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA,
vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA
}),
}, extra=vol.ALLOW_EXTRA)
@@ -130,10 +139,10 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({
# Service call validation schema
MQTT_PUBLISH_SCHEMA = vol.Schema({
vol.Required(ATTR_TOPIC): valid_publish_topic,
vol.Exclusive(ATTR_PAYLOAD, 'payload'): object,
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, 'payload'): cv.string,
vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): object,
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string,
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
}, required=True)
@@ -196,7 +205,7 @@ def _setup_server(hass, config):
server = prepare_setup_platform(hass, config, DOMAIN, 'server')
if server is None:
_LOGGER.error('Unable to load embedded server.')
_LOGGER.error("Unable to load embedded server")
return None
success, broker_config = server.start(hass, conf.get(CONF_EMBEDDED))
@@ -221,7 +230,7 @@ def setup(hass, config):
# Embedded broker doesn't have some ssl variables
client_key, client_cert, tls_insecure = None, None, None
elif not broker_config and not broker_in_conf:
_LOGGER.error('Unable to start broker and auto-configure MQTT.')
_LOGGER.error("Unable to start broker and auto-configure MQTT")
return False
if broker_in_conf:
@@ -241,15 +250,18 @@ def setup(hass, config):
certificate = os.path.join(os.path.dirname(__file__),
'addtrustexternalcaroot.crt')
will_message = conf.get(CONF_WILL_MESSAGE)
birth_message = conf.get(CONF_BIRTH_MESSAGE)
global MQTT_CLIENT
try:
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive,
username, password, certificate, client_key,
client_cert, tls_insecure, protocol)
client_cert, tls_insecure, protocol, will_message,
birth_message)
except socket.error:
_LOGGER.exception("Can't connect to the broker. "
"Please check your settings and the broker "
"itself.")
"Please check your settings and the broker itself")
return False
def stop_mqtt(event):
@@ -274,7 +286,7 @@ def setup(hass, config):
except template.jinja2.TemplateError as exc:
_LOGGER.error(
"Unable to publish to '%s': rendering payload template of "
"'%s' failed because %s.",
"'%s' failed because %s",
msg_topic, payload_template, exc)
return
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
@@ -296,13 +308,14 @@ class MQTT(object):
def __init__(self, hass, broker, port, client_id, keepalive, username,
password, certificate, client_key, client_cert,
tls_insecure, protocol):
tls_insecure, protocol, will_message, birth_message):
"""Initialize Home Assistant MQTT client."""
import paho.mqtt.client as mqtt
self.hass = hass
self.topics = {}
self.progress = {}
self.birth_message = birth_message
if protocol == PROTOCOL_31:
proto = mqtt.MQTTv31
@@ -329,7 +342,11 @@ class MQTT(object):
self._mqttc.on_connect = self._mqtt_on_connect
self._mqttc.on_disconnect = self._mqtt_on_disconnect
self._mqttc.on_message = self._mqtt_on_message
if will_message:
self._mqttc.will_set(will_message.get(ATTR_TOPIC),
will_message.get(ATTR_PAYLOAD),
will_message.get(ATTR_QOS),
will_message.get(ATTR_RETAIN))
self._mqttc.connect(broker, port, keepalive)
def publish(self, topic, payload, qos, retain):
@@ -365,7 +382,8 @@ class MQTT(object):
def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code):
"""On connect callback.
Resubscribe to all topics we were subscribed to.
Resubscribe to all topics we were subscribed to and publish birth
message.
"""
if result_code != 0:
_LOGGER.error('Unable to connect to the MQTT broker: %s', {
@@ -387,6 +405,11 @@ class MQTT(object):
# qos is None if we were in process of subscribing
if qos is not None:
self.subscribe(topic, qos)
if self.birth_message:
self.publish(self.birth_message.get(ATTR_TOPIC),
self.birth_message.get(ATTR_PAYLOAD),
self.birth_message.get(ATTR_QOS),
self.birth_message.get(ATTR_RETAIN))
def _mqtt_on_subscribe(self, _mqttc, _userdata, mid, granted_qos):
"""Subscribe successful callback."""
@@ -404,7 +427,7 @@ class MQTT(object):
"MQTT topic: %s, Payload: %s", msg.topic,
msg.payload)
else:
_LOGGER.debug("received message on %s: %s",
_LOGGER.debug("Received message on %s: %s",
msg.topic, payload)
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
ATTR_TOPIC: msg.topic,
@@ -440,14 +463,14 @@ class MQTT(object):
while True:
try:
if self._mqttc.reconnect() == 0:
_LOGGER.info('Successfully reconnected to the MQTT server')
_LOGGER.info("Successfully reconnected to the MQTT server")
break
except socket.error:
pass
wait_time = min(2**tries, MAX_RECONNECT_WAIT)
_LOGGER.warning(
'Disconnected from MQTT (%s). Trying to reconnect in %ss',
"Disconnected from MQTT (%s). Trying to reconnect in %s s",
result_code, wait_time)
# It is ok to sleep here as we are in the MQTT thread.
time.sleep(wait_time)

View File

@@ -4,41 +4,21 @@ Support for a local MQTT broker.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mqtt/#use-the-embedded-broker
"""
import asyncio
import logging
import tempfile
import threading
from homeassistant.core import callback
from homeassistant.components.mqtt import PROTOCOL_311
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.util.async import run_coroutine_threadsafe
REQUIREMENTS = ['hbmqtt==0.7.1']
DEPENDENCIES = ['http']
@asyncio.coroutine
def broker_coro(loop, config):
"""Start broker coroutine."""
from hbmqtt.broker import Broker
broker = Broker(config, loop)
yield from broker.start()
return broker
def loop_run(loop, broker, shutdown_complete):
"""Run broker and clean up when done."""
loop.run_forever()
# run_forever ends when stop is called because we're shutting down
loop.run_until_complete(broker.shutdown())
loop.close()
shutdown_complete.set()
def start(hass, server_config):
"""Initialize MQTT Server."""
from hbmqtt.broker import BrokerException
loop = asyncio.new_event_loop()
from hbmqtt.broker import Broker, BrokerException
try:
passwd = tempfile.NamedTemporaryFile()
@@ -48,29 +28,20 @@ def start(hass, server_config):
else:
client_config = None
start_server = asyncio.gather(broker_coro(loop, server_config),
loop=loop)
loop.run_until_complete(start_server)
# Result raises exception if one was raised during startup
broker = start_server.result()[0]
broker = Broker(server_config, hass.loop)
run_coroutine_threadsafe(broker.start(), hass.loop).result()
except BrokerException:
logging.getLogger(__name__).exception('Error initializing MQTT server')
loop.close()
return False, None
finally:
passwd.close()
shutdown_complete = threading.Event()
@callback
def shutdown_mqtt_server(event):
"""Shut down the MQTT server."""
hass.async_add_job(broker.shutdown())
def shutdown(event):
"""Gracefully shutdown MQTT broker."""
loop.call_soon_threadsafe(loop.stop)
shutdown_complete.wait()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
threading.Thread(target=loop_run, args=(loop, broker, shutdown_complete),
name="MQTT-server").start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_mqtt_server)
return True, client_config

View File

@@ -38,7 +38,7 @@ DEFAULT_VERSION = 1.4
DEFAULT_BAUD_RATE = 115200
DEFAULT_TCP_PORT = 5003
DOMAIN = 'mysensors'
GATEWAYS = None
MYSENSORS_GATEWAYS = 'mysensors_gateways'
MQTT_COMPONENT = 'mqtt'
REQUIREMENTS = [
'https://github.com/theolind/pymysensors/archive/'
@@ -132,9 +132,15 @@ def setup(hass, config):
return gateway
gateways = hass.data.get(MYSENSORS_GATEWAYS)
if gateways is not None:
_LOGGER.error(
'%s already exists in %s, will not setup %s component',
MYSENSORS_GATEWAYS, hass.data, DOMAIN)
return False
# Setup all devices from config
global GATEWAYS
GATEWAYS = {}
gateways = []
conf_gateways = config[DOMAIN][CONF_GATEWAYS]
for index, gway in enumerate(conf_gateways):
@@ -146,17 +152,19 @@ def setup(hass, config):
tcp_port = gway.get(CONF_TCP_PORT)
in_prefix = gway.get(CONF_TOPIC_IN_PREFIX)
out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX)
GATEWAYS[device] = setup_gateway(
ready_gateway = setup_gateway(
device, persistence_file, baud_rate, tcp_port, in_prefix,
out_prefix)
if GATEWAYS[device] is None:
GATEWAYS.pop(device)
if ready_gateway is not None:
gateways.append(ready_gateway)
if not GATEWAYS:
if not gateways:
_LOGGER.error(
'No devices could be setup as gateways, check your configuration')
return False
hass.data[MYSENSORS_GATEWAYS] = gateways
for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate',
'cover']:
discovery.load_platform(hass, component, DOMAIN, {}, config)

View File

@@ -0,0 +1,81 @@
"""
Support for Neato botvac connected vacuum cleaners.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/neato/
"""
import logging
from datetime import timedelta
from urllib.error import HTTPError
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.1.zip'
'#pybotvac==0.0.1']
DOMAIN = 'neato'
NEATO_ROBOTS = 'neato_robots'
NEATO_LOGIN = 'neato_login'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup the Verisure component."""
from pybotvac import Account
hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account)
hub = hass.data[NEATO_LOGIN]
if not hub.login():
_LOGGER.debug('Failed to login to Neato API')
return False
hub.update_robots()
for component in ('sensor', 'switch'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
class NeatoHub(object):
"""A My Neato hub wrapper class."""
def __init__(self, hass, domain_config, neato):
"""Initialize the Neato hub."""
self.config = domain_config
self._neato = neato
self._hass = hass
self.my_neato = neato(
domain_config[CONF_USERNAME],
domain_config[CONF_PASSWORD])
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
def login(self):
"""Login to My Neato."""
try:
_LOGGER.debug('Trying to connect to Neato API')
self.my_neato = self._neato(self.config[CONF_USERNAME],
self.config[CONF_PASSWORD])
return True
except HTTPError:
_LOGGER.error("Unable to connect to Neato API")
return False
@Throttle(timedelta(seconds=1))
def update_robots(self):
"""Update the robot states."""
_LOGGER.debug('Running HUB.update_robots %s',
self._hass.data[NEATO_ROBOTS])
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots

View File

@@ -18,7 +18,7 @@ from homeassistant.util import Throttle
REQUIREMENTS = [
'https://github.com/jabesq/netatmo-api-python/archive/'
'v0.6.0.zip#lnetatmo==0.6.0']
'v0.7.0.zip#lnetatmo==0.7.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -25,9 +25,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components.frontend import add_manifest_json_key
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['https://github.com/web-push-libs/pywebpush/archive/'
'e743dc92558fc62178d255c0018920d74fa778ed.zip#'
'pywebpush==0.5.0', 'PyJWT==1.4.2']
REQUIREMENTS = ['pywebpush==0.6.1', 'PyJWT==1.4.2']
DEPENDENCIES = ['frontend']
@@ -141,11 +139,23 @@ def _load_config(filename):
return None
class JSONBytesDecoder(json.JSONEncoder):
"""JSONEncoder to decode bytes objects to unicode."""
# pylint: disable=method-hidden
def default(self, obj):
"""Decode object if it's a bytes object, else defer to baseclass."""
if isinstance(obj, bytes):
return obj.decode()
return json.JSONEncoder.default(self, obj)
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=JSONBytesDecoder, indent=4, sort_keys=True))
except (IOError, TypeError) as error:
_LOGGER.error('Saving config file failed: %s', error)
return False

View File

@@ -2,7 +2,7 @@
iOS push notification platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.ios/
https://home-assistant.io/ecosystem/ios/notifications/
"""
import logging
from datetime import datetime, timezone
@@ -48,8 +48,8 @@ def get_service(hass, config):
if not ios.devices_with_push():
_LOGGER.error(("The notify.ios platform was loaded but no "
"devices exist! Please check the documentation at "
"https://home-assistant.io/components/notify.ios/ "
"for more information"))
"https://home-assistant.io/ecosystem/ios/notifications"
"/ for more information"))
return None
return iOSNotificationService()

View File

@@ -26,8 +26,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_service(hass, config):
"""Get the NMA notification service."""
response = requests.get(_RESOURCE + 'verify',
params={"apikey": config[CONF_API_KEY]})
parameters = {
'apikey': config[CONF_API_KEY],
}
response = requests.get(
'{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5)
tree = ET.fromstring(response.content)
if tree[0].tag == 'error':
@@ -47,14 +50,15 @@ class NmaNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
data = {
"apikey": self._api_key,
"application": 'home-assistant',
"event": kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
"description": message,
"priority": 0,
'apikey': self._api_key,
'application': 'home-assistant',
'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
'description': message,
'priority': 0,
}
response = requests.get(_RESOURCE + 'notify', params=data)
response = requests.get(
'{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5)
tree = ET.fromstring(response.content)
if tree[0].tag == 'error':

View File

@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['sendgrid==3.6.0']
REQUIREMENTS = ['sendgrid==3.6.2']
_LOGGER = logging.getLogger(__name__)

View File

@@ -55,8 +55,7 @@ def async_create(hass, message, title=None, notification_id=None):
] if value is not None
}
hass.loop.create_task(
hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
@asyncio.coroutine

View File

@@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util
DOMAIN = 'recorder'
REQUIREMENTS = ['sqlalchemy==1.1.2']
REQUIREMENTS = ['sqlalchemy==1.1.3']
DEFAULT_URL = 'sqlite:///{hass_config_path}'
DEFAULT_DB_FILE = 'home-assistant_v2.db'

View File

@@ -94,6 +94,7 @@ def valid_sensor(value):
def _valid_light_switch(value):
return _valid_device(value, "light_switch")
DEVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,

View File

@@ -7,6 +7,7 @@ by the user or automatically based upon automation events, etc.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/script/
"""
import asyncio
import logging
import voluptuous as vol
@@ -40,7 +41,7 @@ _SCRIPT_ENTRY_SCHEMA = vol.Schema({
})
CONFIG_SCHEMA = vol.Schema({
vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA}
vol.Required(DOMAIN): vol.Schema({cv.slug: _SCRIPT_ENTRY_SCHEMA})
}, extra=vol.ALLOW_EXTRA)
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
@@ -72,11 +73,13 @@ def toggle(hass, entity_id):
hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id})
def setup(hass, config):
@asyncio.coroutine
def async_setup(hass, config):
"""Load the scripts from the configuration."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_SCRIPTS)
@asyncio.coroutine
def service_handler(service):
"""Execute a service call to script.<script name>."""
entity_id = ENTITY_ID_FORMAT.format(service.service)
@@ -84,38 +87,48 @@ def setup(hass, config):
if script.is_on:
_LOGGER.warning("Script %s already running.", entity_id)
return
script.turn_on(variables=service.data)
yield from script.async_turn_on(variables=service.data)
scripts = []
for object_id, cfg in config[DOMAIN].items():
alias = cfg.get(CONF_ALIAS, object_id)
script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE])
component.add_entities((script,))
hass.services.register(DOMAIN, object_id, service_handler,
schema=SCRIPT_SERVICE_SCHEMA)
scripts.append(script)
hass.services.async_register(DOMAIN, object_id, service_handler,
schema=SCRIPT_SERVICE_SCHEMA)
yield from component.async_add_entities(scripts)
@asyncio.coroutine
def turn_on_service(service):
"""Call a service to turn script on."""
# We could turn on script directly here, but we only want to offer
# one way to do it. Otherwise no easy way to detect invocations.
for script in component.extract_from_service(service):
turn_on(hass, script.entity_id, service.data.get(ATTR_VARIABLES))
var = service.data.get(ATTR_VARIABLES)
for script in component.async_extract_from_service(service):
yield from hass.services.async_call(DOMAIN, script.object_id, var)
@asyncio.coroutine
def turn_off_service(service):
"""Cancel a script."""
for script in component.extract_from_service(service):
script.turn_off()
# Stopping a script is ok to be done in parallel
yield from asyncio.wait(
[script.async_turn_off() for script
in component.async_extract_from_service(service)], loop=hass.loop)
@asyncio.coroutine
def toggle_service(service):
"""Toggle a script."""
for script in component.extract_from_service(service):
script.toggle()
for script in component.async_extract_from_service(service):
yield from script.async_toggle()
hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on_service,
schema=SCRIPT_TURN_ONOFF_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service,
schema=SCRIPT_TURN_ONOFF_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service,
schema=SCRIPT_TURN_ONOFF_SCHEMA)
hass.services.async_register(DOMAIN, SERVICE_TURN_ON, turn_on_service,
schema=SCRIPT_TURN_ONOFF_SCHEMA)
hass.services.async_register(DOMAIN, SERVICE_TURN_OFF, turn_off_service,
schema=SCRIPT_TURN_ONOFF_SCHEMA)
hass.services.async_register(DOMAIN, SERVICE_TOGGLE, toggle_service,
schema=SCRIPT_TURN_ONOFF_SCHEMA)
return True
@@ -124,6 +137,7 @@ class ScriptEntity(ToggleEntity):
def __init__(self, hass, object_id, name, sequence):
"""Initialize the script."""
self.object_id = object_id
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self.script = Script(hass, sequence, name, self.async_update_ha_state)
@@ -152,10 +166,12 @@ class ScriptEntity(ToggleEntity):
"""Return true if script is on."""
return self.script.is_running
def turn_on(self, **kwargs):
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the script on."""
self.script.run(kwargs.get(ATTR_VARIABLES))
yield from self.script.async_run(kwargs.get(ATTR_VARIABLES))
def turn_off(self, **kwargs):
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn script off."""
self.script.stop()
self.script.async_stop()

View File

@@ -0,0 +1,61 @@
"""Entity to track connections to stream API."""
import asyncio
import logging
from homeassistant.helpers.entity import Entity
class StreamHandler(logging.Handler):
"""Check log messages for stream connect/disconnect."""
def __init__(self, entity):
"""Initialize handler."""
super().__init__()
self.entity = entity
self.count = 0
def handle(self, record):
"""Handle a log message."""
if not record.msg.startswith('STREAM'):
return
if record.msg.endswith('ATTACHED'):
self.entity.count += 1
elif record.msg.endswith('RESPONSE CLOSED'):
self.entity.count -= 1
self.entity.schedule_update_ha_state()
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the logger for filters."""
entity = APICount()
logging.getLogger('homeassistant.components.api').addHandler(
StreamHandler(entity))
yield from async_add_devices([entity])
class APICount(Entity):
"""Entity to represent how many people are connected to stream API."""
def __init__(self):
"""Initialize the API count."""
self.count = 0
@property
def name(self):
"""Return name of entity."""
return "Connected clients"
@property
def state(self):
"""Return current API count."""
return self.count
@property
def unit_of_measurement(self):
"""Unit of measurement."""
return "clients"

View File

@@ -113,6 +113,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
longitude=hass.config.longitude,
units=units,
interval=config.get(CONF_UPDATE_INTERVAL))
forecast_data.update()
forecast_data.update_currently()
except ValueError as error:
_LOGGER.error(error)
@@ -124,7 +125,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for variable in config[CONF_MONITORED_CONDITIONS]:
sensors.append(DarkSkySensor(forecast_data, variable, name))
add_devices(sensors)
add_devices(sensors, True)
class DarkSkySensor(Entity):
@@ -139,8 +140,6 @@ class DarkSkySensor(Entity):
self._state = None
self._unit_of_measurement = None
self.update()
@property
def name(self):
"""Return the name of the sensor."""
@@ -277,8 +276,6 @@ class DarkSkyData(object):
self.update_hourly = Throttle(interval)(self._update_hourly)
self.update_daily = Throttle(interval)(self._update_daily)
self.update()
def _update(self):
"""Get the latest data from Dark Sky."""
import forecastio

View File

@@ -15,7 +15,7 @@ from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['schiene==0.17']
REQUIREMENTS = ['schiene==0.18']
_LOGGER = logging.getLogger(__name__)

View File

@@ -2,7 +2,7 @@
Support for Home Assistant iOS app sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.ios/
https://home-assistant.io/ecosystem/ios/
"""
from homeassistant.components import ios
from homeassistant.helpers.entity import Entity

View File

@@ -113,15 +113,24 @@ class KNXSensorFloatClass(KNXGroupAddress, KNXSensorBaseClass):
self._unit_of_measurement = unit_of_measurement
self._minimum_value = minimum_sensor_value
self._maximum_value = maximum_sensor_value
self._value = None
KNXGroupAddress.__init__(self, hass, config)
@property
def state(self):
"""Return the Value of the KNX Sensor."""
return self._value
def update(self):
"""Update KNX sensor."""
from knxip.conversion import knx2_to_float
super().update()
self._value = None
if self._data:
from knxip.conversion import knx2_to_float
value = knx2_to_float(self._data)
if self._minimum_value <= value <= self._maximum_value:
return value
return None
self._value = value

View File

@@ -14,7 +14,7 @@ from homeassistant.const import CONF_NAME
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['batinfo==0.3']
REQUIREMENTS = ['batinfo==0.4.2']
_LOGGER = logging.getLogger(__name__)

View File

@@ -16,7 +16,7 @@ from homeassistant.util import Throttle
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC)
REQUIREMENTS = ['miflora==0.1.9']
REQUIREMENTS = ['miflora==0.1.12']
_LOGGER = logging.getLogger(__name__)
@@ -41,6 +41,7 @@ SENSOR_TYPES = {
'light': ['Light intensity', 'lux'],
'moisture': ['Moisture', '%'],
'conductivity': ['Conductivity', 'µS/cm'],
'battery': ['Battery', '%'],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({

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