Compare commits

...

501 Commits

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
Paulus Schoutsen 4770888d22 Merge pull request #4166 from home-assistant/dev
0.32
2016-11-05 08:52:57 -07:00
Paulus Schoutsen 1d0f3b930f Version bump to 0.32.0 2016-11-05 08:40:32 -07:00
Paulus Schoutsen 22e2262f8e Merge remote-tracking branch 'origin/master' into dev 2016-11-05 08:40:04 -07:00
William Scanlon 53d1a040d4 Stop Octoprint from logging errors during startup (#4220)
* Fix log errors

* Remove discovery code
2016-11-05 07:55:59 -07:00
Fabian Affolter d7d71c97e2 Make the wind details more robust (weather.openweathermap) (#4215)
* Make the wind details more robust

* Return None if values is not available
2016-11-05 09:15:59 +01:00
Paulus Schoutsen c15fd4323e Disable insteon hub (#4221) 2016-11-04 23:38:27 -07:00
Pascal Vizeli 91227d9a2e Refactory nest component/platforms (#4219)
* Refactory nest component/platforms
2016-11-04 20:22:47 -04:00
jbcodemonkey a3db0ec231 add dimmer slide control to imported isy lights (#4152)
Supported attribute added and checks appear to pass. 🐬
2016-11-04 15:28:22 -06:00
Paulus Schoutsen 18e965c3cd Fix flaky group notify test (#4212) 2016-11-03 22:56:55 -07:00
Paulus Schoutsen 4cc417677e Add link to issue in warning slow entity update (#4211) 2016-11-03 22:45:01 -07:00
Paulus Schoutsen 525d735f21 Warn if fetching properties takes too long (#4208)
* Warn if fetching properties takes too long

* Update entity.py
2016-11-03 21:58:25 -07:00
Paulus Schoutsen e88b98f5fa Clean up tests (#4209) 2016-11-03 21:58:18 -07:00
Lewis Juggins 6f68752d1e Speed up Sonos tests (#4196) 2016-11-03 21:23:37 -07:00
Fabian Affolter 61a0976752 Use port instead of url and fix PEP257 issues (#4192) 2016-11-03 18:43:42 -07:00
Pascal Vizeli d7b3c9c38e Fix log owntrack log flooting (#4198) 2016-11-03 18:42:22 -07:00
jgriff2 a01939c6e9 Fix Synology Camera SSL certificate option (#4201) [BREAKING CHANGE]
* Fix Synology SSL config

* Revert "Fix Synology SSL config"

This reverts commit b8dc2a92abee6249b3dd42c99d0786820ebbeb72.

* Revert "Fix Synology SSL config"

This reverts commit 805e87f3af300a1b7627bb5df0792285fcf38901.

* Fix Synology SSL config
2016-11-03 18:41:32 -07:00
Fabian Affolter c128919b5f Remove globally disabled pylint warnings (#4204) 2016-11-03 18:40:43 -07:00
Pascal Vizeli e5d69feb93 Fix blocking/stack trace with empty list (#4191) 2016-11-03 18:33:18 -07:00
Paulus Schoutsen ee5f228309 Make services yield (#4187)
* Make services yield

* Disable pylint abstract-method check

* add input_select

* add input_slider

* change to async vers.

* fix lint

* yield on add_entities as other components does
2016-11-03 18:32:14 -07:00
John Arild Berentsen 15dde7925a Prevent multiple instances of device initialzed (#4179) 2016-11-03 13:08:23 +01:00
Pascal Vizeli fcf318cf53 Bugfix windows have a other default loop now (#4195)
* Bugfix windows have a other default loop now

* fix handling with 3.4.2 that not support ensure_future

* make the same as ensure_future does

* fix spell

* fix lazy test
2016-11-03 11:07:47 +01:00
Pascal Vizeli c2a5f63b1f Bugfix async Yr.no (#4190) 2016-11-03 11:09:03 +02:00
Fabian Affolter 79fa2d4175 CUPS sensor (#4142)
* Add CUPS sensor

* Use CupsData

* Fix requirement
2016-11-03 09:31:50 +01:00
Erik Eriksson 214a18f08c Support for Dovado routers (#4176)
* Implemented support for the Dovado router

* Update .coveragerc
2016-11-02 21:20:21 -07:00
Ferry van Zeelst ded2ea8b19 Synology DSM sensor (#4156)
* Added Synology DSM Sensor

* Fixed balloobbot's comments

* Fixed mistake (should have run lint and flake8 before committing

* Fixed update mechanisme according to balloobs feedback

* Requesting retest as test failure isn't related to changes made
2016-11-02 21:17:29 -07:00
Paulus Schoutsen 1d100dcac9 Bugfix/frontend group urls (#4185)
* Remove unnecessary sleeps

* Frontend: fix serving index when refreshing view page.
2016-11-02 21:15:23 -07:00
Bart274 a3ae96440b Update the icloud device_tracker (#4081)
* Update the icloud device_tracker

* addressed @kellerza 's comments

* GMTT config needs an entity_id

* renamed services

* fix cookiedir and clean up keep_alive function

* fix travis errors

* forgot a self.

* update devices after initializing the API

* changed wording

* addressed changes from @kellerza

* Syntax error solved

* Update icloud.py

* Only use account of username instead of whole username as default for account name

* use slugify instead of slug for schema

* remove Google Maps Travel Time

* Add comment from original tracker back
2016-11-02 21:07:23 -07:00
devdelay 0235626f40 yet another command_line sensor update (#4184) 2016-11-02 21:00:32 -07:00
Paulus Schoutsen d7dd7df5e7 Update frontend 2016-11-02 20:39:42 -07:00
Nicolas Graziano 1e28851280 Media player BraviaTv : Try to connect only if tv is not in off state. (#4140)
When HA is restart with the TV in off state there was error log every 10s until the TV is set ON.
2016-11-02 19:51:53 -07:00
bestlibre df68de8032 Influxdb sensor state set to unknown if query return no points (#4148)
* Influxdb sensor state set to unknown if query return no points

* Update influxdb.py
2016-11-02 19:50:18 -07:00
Johann Kellerman f3595f790a Async version of Yr.no (#4158)
* initial

* feedback

* More feedback. Still need to fix match_url

* url_match

* split_lines
2016-11-02 19:34:12 -07:00
Johann Kellerman 0d14920758 Component setup error messages with markdown (#3919)
* Remove_dev_link_async

* callback
2016-11-02 19:31:09 -07:00
Paulus Schoutsen 2940fb72fb EntityComponent.add_entities now converts generators to a list (#4183) 2016-11-02 19:24:25 -07:00
Daniel Perna 8e0838adeb Added support for Philips TVs with jointSPACE API (#4157)
* Added support for Philips Tvs with JointSpace API

* Flake + Lint fixes

* Lint be like "lol fu"

* Changes as requested by reviewers, except lib-requirement

* Switched to library-usage

* lint... newline-bingo...
2016-11-02 19:19:53 -07:00
Paulus Schoutsen 4e820ea30a Move mocks to async_start (#4182) 2016-11-02 19:16:59 -07:00
Pascal Vizeli 26490109ac Change event loop on windows (#4075)
* Change event loop on windows

* fix

* split PR

* remove set event loop

* Add paulus suggestion

* fix missing import

* revert stuff from PR Splitting

* fix event loop on test
2016-11-02 21:53:52 +01:00
Georgi Kirichkov e4a713207d Fixes in TP-Link Switch logging 0 values on init (#4026)
* Fixes in TP-Link Switch logging 0 values on init

On init of component the emeter would log to influxdb and possibly other inputs a 0 value, instead of not logging anything.
Initial polling should circumvent that behavior and avoid logging inconsistencies.

* Refactors update call in __init__
2016-11-02 12:23:43 -07:00
Greg Dowling cc0d0a38d7 Get temparature units from vera controller. (#4130)
Alrighty 👯‍♀️
2016-11-02 14:20:44 +01:00
Erik Eriksson afde5a6b26 extracted logic into an external package. monitor more attributes. support for more than one vehicle (#4170) 2016-11-01 22:01:00 -07:00
Lewis Juggins a5fb284717 Add new_device_discovered event (#4132) 2016-11-01 21:52:27 -07:00
Johann Kellerman e487a09190 Remove None value before writing known_devices (#4098)
* Remove None

* Replace null
2016-11-01 21:51:31 -07:00
Jan Losinski 52eb816c62 Introduce a send_delay for pilight component (#4051)
* Add a method to throttle calls to services

This adds CallRateDelayThrottle. This is a class that provides an
decorator to throttle calls to services. Instead of the Throttle in
homeassistant.util it does this by delaying all subsequent calls
instead of just dropping them. Dropping of calls would be bad if we
call services to actual change the state of a connected hardware (like
rf controlled power plugs).

Ihe delay is done by rescheduling the call using
track_point_in_utc_time from homeassistant.helpers.event so it should
not block the mainloop at all.

* Add unittests for CallRateDelayThrottle

Signed-off-by: Jan Losinski <losinski@wh2.tu-dresden.de>

* Introduce a send_delay for pilight component

If pilight is used with a "pilight USB Nano" between the daemon and the
hardware, we must use a delay between sending multiple signals.
Otherwise the hardware will just skip random codes. We hit this
condition for example, if we switch a group of pilight switches on or
off. Without the delay, random switch signals will not be transmitted by
the RF transmitter.

As this seems not necessary, if the transmitter is directly connected
via GPIO, we introduce a optional configuration to set the delay.

* Add unittests for pilight send_delay handling

This adds an unittest to test the delayed calls to the send_code
service.
2016-11-01 21:50:27 -07:00
Jason Carter 90d894a499 Garadget (#4031)
* Initial attempt at implementation

* Adding Garadget cover component

* Updating Device to be Required

* Updating .coveragerc to exclude from testing

* Updating code review items

* Updating per 2nd code review

* Updating configuration to be more like command-line
2016-11-01 21:49:27 -07:00
Jon Caruana ba13951fff Add LiteJet (a lighting control system) component (#4125)
* Initial submission of LiteJet integration.

* Add LiteJet switch pressed automation trigger. (State changes are too slow to catch a press-release.)
Add LiteJet scene, replacing commented out code that treated these as lights.
Include LiteJet numbers in the device state so that it is easy to lookup entity -> number.

* Fix missing global.

* Allow light's brightness to be set explicitly.

* Support optional 'ignore' key to ignore prefixes of loads, switches, and scenes that weren't configured for use in the LiteJet system.

* Fix lint errors and warnings.

* Cleanup header comments.
Default to not creating LiteJet switches as these are generally not useful.

* Lint fixes.

* Fixes from pull request feedback.

* Use hass.data instead of globals for data storage.

* Fix lint warnings.
2016-11-01 20:44:25 -07:00
Sean Dague 2a7b7ebd6a Merge pull request #3985 from postlund/yamaha_additions
Improve support for Yamaha receiver
2016-11-01 21:50:03 -04:00
Bjarni Ivarsson df7d9c3bb2 Fallback to read volume and mute state from speaker. (#4173) 2016-11-01 15:12:18 -07:00
Bjarni Ivarsson c549ea115d Sonos responsiveness improvements + enhancements (#4063)
* Sonos responsiveness improvements (async_ coroutines, event based updating, album art caching) + Better radio station information

* Docstring fixes.

* Docstring fixes.

* Updated SoCo dependency + fixed file permissions.

* Only fetch speaker info if needed.

* PEP8 fixes

* Fixed SoCoMock.get_speaker_info to get test to pass.

* Regenerated requirements_all.txt + async fetching of album art with caching + added http_session to HomeAssistant object.

* Unit test fixed.

* Add blank line as per flake8

* Fixed media image proxy unit test.

* Removed async stuff.

* Removed last remnants of async stuff.
2016-11-01 10:42:38 -07:00
Fabian Affolter dad54bb993 Update to make the sample file validate (#4168) 2016-11-01 15:11:42 +01:00
Fabian Affolter 0211cf29eb Change behavior to be more natural and fix test (#4150) 2016-11-01 10:43:48 +01:00
Nicholas Sideras 1d9ac5f8b3 Update __init__.py (#4155)
Changed manifest.json to respect Android screen rotate lock.
2016-10-31 13:04:54 -07:00
Paulus Schoutsen 7f699b4261 Lazy initialise the worker pool (#4110)
* Lazy initialise the worker pool

* Minimize pool initialization in core tests

* Fix tests on Python 3.4

* Remove passing in thread count to mock HASS

* Tests: Allow pool by default for threaded, disable for async

* Remove JobPriority for thread pool

* Fix wrong block_till_done

* EmulatedHue: Remove unused test code

* Zigbee: do not touch hass.pool

* Init loop in add_job

* Fix core test

* Fix random sensor test
2016-10-31 08:47:29 -07:00
Paulus Schoutsen a1e910f1cf Disable rest switch tests 2016-10-31 08:22:49 -07:00
Fabian Affolter b4899ec469 Allow multiple symbols (sensor.yahoo_finance) (#4126)
* Allow multiple symbols

* Update test
2016-10-31 13:31:09 +01:00
John 06de7053ce Add Emby Server media_player component (#3862)
* Add Emby Server media_player component

* Code cleanup, move to request sessions, generate UUID per session

* Make media image fetch more robust

* Allow for http or https

* Cleanup some Keyerror conditions found through more testing

* Move EmbyRemote to pip, update requirements

* Code cleanup, add SSL config option
2016-10-31 13:29:08 +01:00
Fabian Affolter 4484a7a94b Use voluptuous for Pilight switch (#3819)
* Migrate to voluptuous

* Add protocol

* Update
2016-10-31 13:18:47 +01:00
Jared Beckham a89e635bf3 Added tests for REST switches (#4016)
* Added tests for REST switches

* Remove REST switch from test coverage exclusions
2016-10-31 13:14:23 +01:00
Daniel Høyer Iversen 3ab056ba69 Merge pull request #4149 from home-assistant/flux_led_library
flux led lib
2016-10-31 11:56:00 +01:00
Daniel 5ba815ab21 flux led lib 2016-10-31 09:23:34 +01:00
Fabian Affolter 274e9799b3 Add random number sensor (#4139) 2016-10-31 00:01:25 -07:00
Jared Beckham 5ce9aea65d Added tests for REST sensors (#4115) 2016-10-30 21:51:03 -07:00
Michael 705814cb08 Catch all errors when doing mqtt message unicode-decode. (#4143)
* catch all errors when doing mqtt message unicode-decode.

* added AttributeError and UnicodeDecodeError to exception when decoding an mqtt message payload
2016-10-30 23:17:41 +01:00
Alok Saboo 8e695d1eb0 Fixed typo (#4145) 2016-10-30 23:13:27 +01:00
Fabian Affolter be272ac64a Disable too-many-* (#4107)
* Disable too-many-* and too-few-public-methods

* Remove globally disabled pylint warnings
2016-10-30 22:18:53 +01:00
Fabian Affolter b910a9917d Migrate to async (sensor.statistics) (#4138)
* Migrate to async

* Add async_ prefix and remove stale print
2016-10-30 18:56:26 +01:00
Fabian Affolter 9649097b32 Migrate to async (sensor.min_max) (#4136)
* Migrate to async

* Add async_ prefix
2016-10-30 16:45:53 +01:00
Fabian Affolter 5e76a51db4 Migrate to async (#4135) 2016-10-30 15:23:47 +01:00
Fabian Affolter 27abac85b6 Migrate to async (sensor.time_date) (#4100)
* Migrate to async

* Update acc. #4114
2016-10-30 15:21:23 +01:00
Fabian Affolter 9f2aae1357 Maintenance 2nd (#4106)
* Add link to docs

* Fix link

* Update line breaks

* Update ordering

* Align vera platofrm to only use add_devices
(instead od add_devices_callback)

* Remove line break

* Use consts

* Update ordering

* Update ordering

* Use const, create default name, use string formatting

* Update ordering

* Use const

* Update import style

* Update ordering and line breaks

* update line breaks

* Set default port

* Set defaults and update ordering

* Update ordering

* Minor style updates

* Update ordering, defaults, line breaks, and readability

* Use constants

* Add line breaks

* use string formatting

* Update line breaks

* Update logger
2016-10-30 09:58:34 +01:00
Adam Mills e6ece4bf6d Fix initialization of zwave color bulbs (#4085)
* Fix initialization of zwave color bulbs

Zwave values can be added to the node in any order. This change allows
proper initialization when the multilevel value is added before the
color value.

* Fix incorrect rename of color command class
2016-10-29 17:14:28 -07:00
Hydreliox aea2d1b317 Add support for Yeelight Wifi bulbs (#4065)
* Add support for Yeelight Wifi bulbs

* Fix cache property in instance
2016-10-29 17:03:26 -07:00
Pierre Ståhl 33e46b484f Add service to change visibility of a group (#3998) 2016-10-29 16:54:26 -07:00
wokar 3f6a5564ad lg_netcast platform fails to load if no channels defined (#4083)
* fixes loading of lg_netcast platform if no channels are defined

* turned list comprehension into for loop
2016-10-29 16:52:53 -07:00
Erik Eriksson 3317b4916b OSError is alias for IOException and base class for many other exceptions - no need to catch redundant exceptions if OSError already present in except-clause (#4111) 2016-10-29 15:33:56 -07:00
Pascal Vizeli 9c0455e3dc Allow update entities on add_entities callback (#4114)
* Allow udpate entities on add_entities callback

* fix wrong position

* update force_update to update_before_add

* add unittest for update_befor_add

* fix unittest

* change mocking
2016-10-29 15:33:11 -07:00
Fabian Affolter 5d43d3eb1c Fix error message (#4122) 2016-10-29 15:30:23 -07:00
Paulus Schoutsen 4163e55dbd Introducing hass.data (#4121)
* Hello hass.data

* Migrate setup_component to hass.data
2016-10-29 14:51:17 -07:00
Paulus Schoutsen 3cc4fdaa34 Fix HTTP static file singular (#4118) 2016-10-29 14:45:31 -07:00
Pascal Vizeli edeb31d74e Fix bug with aioHTTP and none authentification (#4116) 2016-10-29 22:47:46 +02:00
Fabian Affolter 54d19e3c53 Maintenance (sensor.currencylayer, sensor.fixer) (#4103)
* Add new const (base)

* Use constant

* Remove second error message, use const, add attribution, add link
to docs, remove unused vars, and a little simplification

* Add quote

* Use const

* Add attribution, simplify the code, and use consts
2016-10-29 13:27:02 -07:00
Fabian Affolter 892f455aee Maintenance (sensor.bitcoin, sensor.yahoo_finance) (#4104)
* Add attribution

* Update ordering

* Update ordering
2016-10-29 13:21:09 -07:00
Fabian Affolter 942d630762 Maintenance zoneminder (#4102)
* Add timeout to requests, fix typos, and defaults

* Clean-up
2016-10-29 13:10:42 -07:00
Paulus Schoutsen 9ea1101aba Fix bootstrap circular imports (#4108)
* Fix bootstrap circular imports

* fix test

* Lint
2016-10-29 12:54:47 -07:00
Pascal Vizeli 08a65a3b31 Async input_*/zone migration (#4095)
* Async input_*

* Async zone component

* rename service callback
2016-10-29 12:19:27 -07:00
Fabian Affolter d4b3f56d53 Maintenance (#4101)
* UPdate ordering, fix typos, and align logger messages

* Update import style, fix PEP257 issue, and align logger messages

* Updaate import style and align logger messages

* Update import style and align logger messages

* Update ordering

* Update import style and ordering

* Update quotes

* Make logger messages more clear

* Fix indentation
2016-10-29 09:12:43 -07:00
Paulus Schoutsen 5a2b4a5376 Core Async improvements (#4087)
* Clean up HomeAssistant.start

* Add missing pieces to remote HA constructor

* Make HomeAssistant constructor async safe

* Code cleanup

* Init websession lazy
2016-10-29 08:57:59 -07:00
Sean Dague 9d836a115a Add zone_ignore option for yamaha. (#4091)
* Add zone_ignore option for yamaha.

We attempt to discover all zones for yamaha receivers. There are times
when users may want to suppress some zones from showing up. When a
Zone isn't actually connected to speakers, or on some newer receivers
where Zone_4 is an HDMI only zone, that doesn't support even basic
media_player UI.

This provide a mechanism for users to do that.

Fixes #4088

* Update yamaha.py
2016-10-28 19:18:31 -07:00
Fabian Affolter bf92aedd38 Add hddtemp sensor (#4092) 2016-10-28 19:06:24 -07:00
devdelay 230c3815f2 Update command_line sensor to use STATE_UNKNOWN (#4093) 2016-10-28 19:03:40 -07:00
Pascal Vizeli 9afe066ec8 Fix name in openalpr cloud api (#4097) 2016-10-28 19:01:14 -07:00
Pascal Vizeli 66541a6a19 Update ha-ffmpeg to version 0.15 (#4096) 2016-10-29 00:12:53 +02:00
Pascal Vizeli 825ee3612d fix some comments spell (#4082)
* fix some comments

* fix in an executor

* address paulus comments
2016-10-28 21:26:52 +02:00
Abhishek Anand 65bd7d2326 Generalized REST switch to enable templating and configurable timeout. (#3329)
* successfully tested the "remote temperature mode" switch for the radio thermostat

* removed logging and interpreted None as Off.

* turn_off value is also templated now -- can depend on state

Also, undid accidental removal of error logging.

* ensured backward compatibility of config file

if value_template is not provided, the update function behaves as before

* ran autopep8 --in-place

* fixed another complaint of tox

* addressed the comments of balloob

* undid acccidental log.error to log.info

* timeout : 50 -> 10

* added a timeout parameter

* removed the stray '-', better names for the failure case

* string comparisons after .lower(), as suggested by balloob

* addressed balloob's latest requests

* making flake happy

* value_template --> is_on_template in config file

* moved CONF_IS_ON_TEMPLATE to local file

* null checks

* addressed flake error

* properly comparing template text when is_on is not a template.
2016-10-27 22:34:22 -07:00
John Arild Berentsen d8c1013b09 Zwave climate, add operating state to attributes (#4069)
* Zwave climate, add operating state to attributes

* Reversed assisgnment
2016-10-27 22:25:17 -07:00
Fabian Affolter 02d1dc6247 Upgrade psutil to 4.4.2 (#4079) 2016-10-27 22:22:43 -07:00
Paulus Schoutsen 726d950522 Update aiohttp.py 2016-10-27 21:45:35 -07:00
Pascal Vizeli 3324995e70 Async clientsession / fix stuff on aiohttp and camera platform (#4084)
* add websession

* convert to websession

* convert camera to async

* fix lint

* fix spell

* add import

* create task to loop

* fix test

* update aiohttp

* fix tests part 2

* Update aiohttp.py
2016-10-27 21:40:10 -07:00
Fabian Affolter 85747fe2ef Upgrade python-telegram-bot to 5.2.0 (#4080) 2016-10-27 21:28:09 -07:00
Pascal Vizeli 09db875ace Fix async bug in automation (#4078) 2016-10-27 18:26:55 +02:00
bestlibre 7d407756c3 Converting unit_of_measurement variable to optional, to be consistent with other sensors (#4076) 2016-10-27 08:50:36 -07:00
Richard Cox 91d682d02c Adding ssl option to zoneminder (#4074) 2016-10-27 15:54:03 +02:00
Benoit BESSET b75c103db4 fixed Up/Down (#4064) 2016-10-27 12:10:38 +02:00
Richard Cox bba323d226 [media_player/onkyo] host should be optional (#4073) 2016-10-27 10:33:35 +02:00
Paulus Schoutsen 7564d539c1 Lint 2016-10-27 00:37:02 -07:00
Paulus Schoutsen 33439aaa22 Update frontend 2016-10-27 00:21:55 -07:00
Pascal Vizeli d5368f6f78 Async bootstrap / component init (#3991)
* Async bootstrap

* Adress comments

* Fix tests

* More fixes

* Tests fixes
2016-10-27 00:16:23 -07:00
Simon Szustkowski d9999f36e8 Added a ThingSpeak component (#4027)
* Added a ThingSpeak component

* Forgot a colon. Fixed it

* Some config variables are better required

* New requirements created by the script

* Updated the .coveragerc

* Fixed small linting errors

* Removed unneccessary validation

* Even more linting error fixes

* Changed the way the component listens to state changes

* Removed unneccessary declaration of 'state' variable, referring to new_state instead
2016-10-26 23:56:51 -07:00
Pierre Ståhl 235e1a0885 Minor improvements to RPi camera platform (#4059)
* Try to create output file instead of checking write permissions

* Kill raspistill process during shutdown
2016-10-26 23:51:13 -07:00
Paulus Schoutsen 541fec0534 Sort .coveragerc alphabetically. 2016-10-26 23:50:11 -07:00
bestlibre b3ad7989ae Influxdb sensor (#4060)
* Influxdb sensor with voluptuous configuration validation

* Adding sensor to coveragerc since there is no test for now
2016-10-26 23:48:57 -07:00
Sean Dague 3d897e0e52 Add discovery for yamaha component (#4061)
This uses the discovery code from netdisco/ha to discover yamaha
receivers. The old discovery code remains if discovery is turned of in
HA, at least for now. Though it probably is worth turning that off in
the future.
2016-10-26 23:46:44 -07:00
Alok Saboo c6d5987109 Create Currencylayer exchange rate sensor (#4062)
* Added Currencylayer exchange rate sensor

* Updated .coveragerc to include currencylayer

* Update currencylayer.py

* Added Conf_name
2016-10-26 23:46:13 -07:00
Matthew Treinish 5d3956ea98 Cleanup use of MQTT in emulated_hue tests (#4068)
* Use unix newlines on test_emulated_hue

This commit switches the test_emulated_hue module to use unix newlines
instead of the DOS style that were there before. (using dos2unix on
the file) This makes it consistent with the other files in the repo.

* Cleanup emulated_hue tests

Previously these tests relied on the mqtt light platform as test devices
to control with the emulated hue. However, this was pretty heavyweight
and required running an MQTT broker in the tests. Instead this commit
switches it to use the demo light platform which is strictly in memory.

Fixes #3549
2016-10-26 23:33:43 -07:00
Marcelo Moreira de Mello 4fb0b27310 Wunderground sensor with alerts exceeds API limits (#4070)
* Fixes issue #4067 - Wunderground sensor with alerts exceeds API limits

 To avoid hitting the max limit of 500 calls per day, this patch keeps weather conditions being updated each 5 minutes
  and weather advisories each 15 minutes.

 This formula will result the following:

   conditions -> 300 seconds -> 5 minutes -> 12 req/h -> 288 req/day
   alerts -> 900 seconds -> 15 minutes -> 4 req/h -> 96 req/day

* Using timedelta in minutes instead seconds
2016-10-26 23:31:49 -07:00
Paulus Schoutsen 4833e992fb Pin cython==0.24.1 (#4057) 2016-10-25 23:38:32 -07:00
Paulus Schoutsen fe3aed0f0c Update .coveragerc 2016-10-25 23:32:58 -07:00
Paulus Schoutsen 57402bcb43 Update .coveragerc 2016-10-25 23:30:43 -07:00
Scott O'Neil 7f48c00793 Adding timer setting functionality to sonos component (#3941)
* Adding timer setting functionality to sonos component

* Adding clear sleep timer for Sonos
2016-10-25 23:22:17 -07:00
Paulus Schoutsen f58647849a Fix Z-Wave: Pin cython in Dockerfile (#4055) 2016-10-25 23:17:34 -07:00
Marcelo Moreira de Mello 1f468fc94d If no weather advisories were issued, state should return 0 instead Unknown (#4029)
* If no weather advisories were issued, state should return 0 instead Unknown

* Updated to keep on the same if statement

* Revert "Updated to keep on the same if statement"

This reverts commit 0e6a94aa0f.
2016-10-25 22:49:51 -07:00
Bjarni Ivarsson 961c02f72a Sonos improvements (#3997)
* Sonos improvements: media_* properties delegate to coordinator if speaker is a slave, media_image_url and media_title now works for radio streams, source selection/list takes speaker model into account, commands on slaves delegate to coordinator.

* Fixed failing unit tests.
2016-10-26 00:37:47 +02:00
Robbie Trencheny 79da1ec0d9 Merge pull request #4037 from home-assistant/remove-deprecated-things
Remove deprecated things
2016-10-25 14:28:01 -07:00
Robbie Trencheny fe174402d2 Remove more deprecated things 2016-10-25 14:16:08 -07:00
Matthew Treinish 1b2dfb8ed1 Handle FreeBSD version in updater component (#4048) 2016-10-25 22:38:22 +02:00
Pierre Ståhl 297a6f6f03 Improve support for Yamaha receiver
* Playback (play, pause, stop, next, previous)

* Media title, artist and album
2016-10-25 20:53:04 +02:00
Per Sandström 0dfcf40d37 [WIP] Config validation error line numbers (#3976)
Config validation error line numbers
2016-10-25 20:13:32 +02:00
Fabian Affolter d308ea69ce Upgrade yahoo-finance to 1.3.2 (#4040) 2016-10-25 08:36:20 -07:00
Sean Dague a8c5c995a0 Merge pull request #4042 from sdague/typo
fix typos in script module strings
2016-10-25 07:40:11 -04:00
Sean Dague 86b318e992 fix typos in script module strings
It looks like some copy / paste in docstrings, clean them up for
posterity.
2016-10-25 07:20:40 -04:00
Bart274 53ea926292 Fix for see service attributes (#4023) 2016-10-25 10:59:20 +02:00
Robbie Trencheny 2b1f4123db Update requirements_all.txt 2016-10-24 22:36:04 -07:00
Robbie Trencheny b36e346ccb Fix helpers.state tests 2016-10-24 22:33:54 -07:00
Adam Mills 89e8fb4066 Configurator support for entity_picture (#4028) 2016-10-24 22:28:34 -07:00
David-Leon Pohl e2d23d902a Unittests for ddwrt device tracker and bugfix (#3996)
* BUG Message data cannot be changed thus use voluptuous to ensure format

* Pilight daemon expects JSON serializable data

Thus dict is needed and not a mapping proxy.

* Add explanation why dict as message data is needed

* Use more obvious voluptuous validation scheme

* Pylint:  Trailing whitespace

* Pilight sensor component

* Python 3.4 compatibility

* D202

* Use pytest-caplog and no unittest.TestCase

* Fix setup/teardown of unittests

* Activate coverage testing

* Bugfix whitelist filter and use bugfixed pilight library

* Use newest pilight library that has a bugfix

* Add unittest for pilight hub component

* PEP257 for docstrings

* Bugfix setting device name from host name and small cleanup

- Init with connection error handling is more clear
- Comments clean-up

* PEP257

* New unittest with full coverage

* Upload missing testfixtures

* D209

* Handle double quotes in reply

* Formatting
2016-10-24 22:18:24 -07:00
Fabian Affolter 2604dd89a6 Add test (#3999) 2016-10-24 22:01:38 -07:00
Robbie Trencheny 044b9caa76 Remove garage_door, hvac, rollershutter and thermostat components/platforms 2016-10-24 22:00:43 -07:00
Daniel Perna 1707cdf9f3 Added support for Notifications for Android TV / FireTV (#3978)
* Added support for Notifications for Android TV / FireTV

* Silly me forgot to commit coverage

* Fixed pylint

* Fixed flake8

* Fixed another flake8 -.-

* Changed option 'ip' to 'host' like most other platforms do
2016-10-24 21:59:09 -07:00
Fabian Affolter 4c86721e70 Update tests, rename variable, and change conversion (#3546) 2016-10-24 21:53:03 -07:00
Matthew Treinish 0ff500ca25 Add mochad component (#3970)
This commit adds a new component for communicating with mochad[1] a
socket interface for the CM15A and CM19A USB X10 controllers. This
commit leverages the pymochad library to interface with a mochad socket
either on a local or remote machine. Mochad is added as as a generic
platform because it supports multiple different classes of device,
however in this patch only the switch device implemented as a starting
point. Future patches will include other devices types. (although
that's dependent on someone gaining access to those)

[1] https://sourceforge.net/projects/mochad/
2016-10-24 21:49:49 -07:00
Robbie Trencheny 23f54b07c7 Dont load notify.ios if no devices exist. Thanks @arsaboo for catching this. 2016-10-24 18:46:47 -07:00
Robbie Trencheny f25ddef4d7 More iOS HTTP Async updates 2016-10-24 17:31:45 -07:00
Robbie Trencheny 7158919346 Missed a wsgi->http on iOS component 2016-10-24 15:03:51 -07:00
Fabian Affolter 627517cbbc Upgrade psutil to 4.4.0 (#4032) 2016-10-24 22:24:33 +02:00
Fabian Affolter 4ecfc7d066 Upgrade sqlalchemy to 1.1.2 (#4003) 2016-10-24 22:00:22 +02:00
Fabian Affolter 72751b95b5 Upgrade slacker to 0.9.29 (#4000) 2016-10-24 22:00:02 +02:00
Fabian Affolter fc3b7907ed Upgrade python-telegram-bot to 5.1.1 (#4001) 2016-10-24 21:59:48 +02:00
Paulus Schoutsen f26a7fc6bb Update http 2016-10-24 00:09:20 -07:00
Robbie Trencheny 0c563f7b14 Minor updater... updates (#4020)
* Enable updater in dev versions

* Code clarity

* Add log line about being on the current version already

* Remove dev check test
2016-10-24 00:01:56 -07:00
Paulus Schoutsen 519d9f2fd0 async HTTP component (#3914)
* Migrate WSGI to asyncio

* Rename wsgi -> http

* Python 3.4 compat

* Move linting to Python 3.4

* lint

* Lint

* Fix Python 3.4 mock_open + binary data

* Surpress logging aiohttp.access

* Spelling

* Sending files is a coroutine

* More callback annotations and naming fixes

* Fix ios
2016-10-23 23:48:01 -07:00
Paulus Schoutsen 3701ac292c Merge pull request #4017 from home-assistant/release-0-31-1
0.31.1
2016-10-23 21:57:38 -07:00
Lewis Juggins 1db18478d2 Exclude dirs/files prefixed with . (#3986) 2016-10-23 21:08:56 -07:00
Robbie Trencheny 3230869f74 Fix a spelling problem on user-facing error 2016-10-23 20:34:31 -07:00
Robbie Trencheny 9aa88819a5 Fix a spelling problem on user-facing error 2016-10-23 20:33:49 -07:00
Paulus Schoutsen 3e92318cb2 Version bump to 0.31.1 2016-10-23 19:20:30 -07:00
Johann Kellerman f0a38dded6 Catch UnicodeDecodeError Error (#4007)
* Catch UnicodeDecodeError Error

Error for #3933

* Forgot (exc)

* catch...

* Tests by @lwis

* Docstring

* Create open
2016-10-23 19:20:13 -07:00
Robbie Trencheny c32f47aea6 iOS component hotfixes (#4015)
* iOS component hot fixes around component/platform loading, logging, and more

* Load device_tracker and zeroconf in deps instead of bootstraping

* Change conditional check on status code
2016-10-23 19:18:13 -07:00
Robbie Trencheny 626763a7c3 iOS component hotfixes (#4015)
* iOS component hot fixes around component/platform loading, logging, and more

* Load device_tracker and zeroconf in deps instead of bootstraping

* Change conditional check on status code
2016-10-23 19:17:34 -07:00
Johann Kellerman 5df8477536 Catch UnicodeDecodeError Error (#4007)
* Catch UnicodeDecodeError Error 

Error for #3933

* Forgot (exc)

* catch...

* Tests by @lwis

* Docstring

* Create open
2016-10-23 18:55:06 -07:00
Fabian Affolter 1f89e6ddba Upgrade pytz to 2016.7 (#4002) 2016-10-23 20:51:41 +02:00
Lewis Juggins 13ab2be5f6 Exclude dirs/files prefixed with . (#3986) 2016-10-23 16:47:06 +02:00
Robbie Trencheny 2bc84af87e Version bump to 0.32.0.dev0 2016-10-22 15:23:01 -07:00
Robbie Trencheny ef2ed7bfc9 Merge pull request #3937 from home-assistant/dev
0.31
2016-10-22 15:22:07 -07:00
Robbie Trencheny 6040a40af2 Update version 2016-10-22 15:09:05 -07:00
jbags81 fb352c20d9 Update wink.py (#3957)
* Update wink.py

added lambda and smoke detector call in component loading routine to fix broken functionality.

* Update wink.py

fixed extra space.

* Update wink.py

applied cleaner refactor per comments

* Update wink.py

fixed spacing

* Update wink.py

fixed lint error #1
2016-10-22 16:59:20 -04:00
John Arild Berentsen 678f30def1 Prevent Verisure cam to delete a file when it is None (#3988) 2016-10-22 21:01:12 +02:00
Paulus Schoutsen 0fce5ccc7f Update frontend 2016-10-22 11:24:06 -07:00
John Arild Berentsen 02afc98668 Prevent zwave from firing event at shutdown (#3987) 2016-10-22 14:08:24 +02:00
Eric Hagan 57777ef79a Adds support for Pioneer AVR interface port number (#3878)
* Adds support for Pioneer AVR interface port number

https://community.home-assistant.io/t/support-for-pioneer-avr/503
telnetlib supports a port number so adding port as
an optional config element with a default of 23 resolves this.

* Adds timeout to Pioneer AVR

timeout in telnetlib defaults to socket._GLOBAL_DEFAULT_TIMEOUT
which is not a value, but rather a bare Object used for comparison.

telnetlib says the following about the timeout optional argument:
"The optional timeout parameter specifies a timeout in seconds
 for blocking operations like the connection attempt (if not
 specified, the global default timeout setting will be used)."

From the documentation for sockets:
"Sockets are by default always created in blocking mode"

Catching connect and timeout errors, logging to debug
and continuing.

* Catches timeout exceptions, logs and continues.
2016-10-22 11:05:00 +02:00
Paulus Schoutsen ca6fa1313e Fix updater, add new fields (#3982)
* Fix updater

* Add Docker and virtualenv checks

* Add log line informing user of update/analytics

* Remove str
2016-10-21 23:30:40 -07:00
Robbie Trencheny ea91d24eb2 HA iOS support (#3752)
* Initial commit of the iOS component and platform

* Allow extra

* Add battery to identify, a new function to get devices, and load the upcoming sensor

* Add iOS sensor platform, currently for battery state & level

* Add discoverability for the iOS app

* Convert single quote to double quotes

* Load all required components and platforms when loading the iOS component for the best experience

* Unify quote style to double

* Change to hass_ios

* Update push URL, add support for logging based on status code, log rate limit updates

* Block iOS from coverage checks for now...
2016-10-21 23:20:15 -07:00
Brent Hughes 6e5a3c0a94 Fixed statsd stopping if state is not numeric or only attributes changed (#3981)
* Fixed statsd stopping if attribute changed by not the state

* Fixed tests which exposed a new bug.

* Fixed another issue. whoops
2016-10-21 23:18:13 -07:00
dasos 754d536974 Work better with password-protected Squeezebox / LMS servers (#3953)
* Work better with password-protected Squeezebox / LMS servers, including getting file art. Refactored to only have a single method calling the telent lib. (Should make it easier to convert to the more appropriate JSON interface)

* Update squeezebox.py

* Update squeezebox.py

* Update squeezebox.py

* Update squeezebox.py

* Update squeezebox.py
2016-10-21 22:37:35 -07:00
Georgi Kirichkov 4f6ed09a99 Adds energy monitoring capabilities to the TP-Link HS110 (#3917)
* Adds energy monitoring capabilities to the TP-Link HS110

Energy monitoring works only on the HS110 model

* Reverts to using GadgetReactor's module

* Updates requirements_all.txt

* Refactors tplink switch to use attribute caching

* Update tplink.py
2016-10-21 21:45:36 -07:00
Johann Kellerman 8d375e2d47 Improve known_device.yaml writing (#3955)
* Better known_device.yaml writing

* yaml dump
2016-10-21 21:41:27 -07:00
Hydreliox 1d2d338cd0 Add Bbox Router bandwidth as sensors (#3956)
* Add Bbox Routeur bandwidth as sensors

Add possibility to monitor max and currently used bandwidth of your xdsl connection for Bbox Routeur

* Minor Fixes

Unit constant get back into the main sensor file

* Unused round removed
2016-10-21 21:34:22 -07:00
Hugo Dupras cb47507002 Add support for Neato Connected robot as a switch (#3935)
* Add support for Neato Connected robot as a switch

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

* Add checklist items

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

* Add missing docstring

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

* [Neato] Add update function to retrieve robot state

* Add docstring for update function + catch exception when retrieving state

* Change type of HTTPError when updating state

* Fix pylint errors
2016-10-21 21:14:45 -07:00
Fabian Affolter fae620f3b3 Migrate to voluptuous (#3748) 2016-10-21 21:14:35 -07:00
Paulus Schoutsen b821a82417 Merge remote-tracking branch 'origin/master' into dev 2016-10-21 20:57:07 -07:00
Paulus Schoutsen da7837af73 Update frontend 2016-10-21 20:52:58 -07:00
Johann Kellerman 6e903fd429 Updater component - rename opt_out to reporting (#3979)
* Updater component - rename opt_out to reporting

* Fix tests
2016-10-21 20:40:23 -07:00
Martin Hjelmare 9f7e167669 Fix test using libsodium and SECRET_KEY (#3975)
* Move test to class with custom config setups and with config
  validation.
2016-10-21 20:23:29 -07:00
John Arild Berentsen 54a64fb8d9 Add support for verisure file camera. (#3952) 2016-10-21 22:41:17 +02:00
Nick Vella 2d89c3ecf4 Telstra SMS API notification component (#3949)
* Telstra API component

* import PLATFORM_SCHEMA

* Exclude Telstra notify component in coveragerc

* fix authentication issues

* Include title in SMS if it's provided

* pass lint

* Fix many code styling issues

* Confirm credentials are correct on component setup
2016-10-20 23:47:21 -07:00
Teemu Mikkonen 9f6d1c4e7b Device tracker: SNMPv3 (#3961)
* Add initial SNMPv3 support for better security

* Fixed indentation errors

* Fixed flake8 E128 on row 65

* Disabled warning about too many instance-attributes

* Removed extra code, added Inclusive to make better config validation
2016-10-20 23:01:30 -07:00
AlucardZero 62b8e54235 Upgrade SoCo to 0.12 (#3951)
Changelog: https://github.com/SoCo/SoCo/releases/tag/v0.12

Backwards Compatability changes:

    Dropped support for Python 3.2
    Methods relating to the music library (get_artists, get_album_artists, get_albums and others) have been moved to the music_library module. Instead of device.get_album_artists(), please now use device.music_library.get_album_artists() etc. Old code will continue to work for the moment, but will raise deprecation warnings
    Made a hard deprecation of the Spotify plugin since the API it relied on has been deprecated and it therefore no longer worked
    Dropped pylint checks for Python 2.6
2016-10-20 22:51:00 -07:00
Dustin S 1ceac8407d Adding security contexts to the resources. (#3840) 2016-10-20 22:43:39 -07:00
Jason Carter 0c0b02eb3d Moving updates out of state property (#3966) 2016-10-20 22:35:25 -07:00
Johann Kellerman c70722dbae Updater component with basic system reporting (#3781) 2016-10-20 21:30:44 +02:00
Martin Hjelmare a05fb4cef8 Upgrade pymysensors to 0.8 (#3954) 2016-10-20 20:56:26 +02:00
Marcelo Moreira de Mello fee01fcccc Added unit test to the Yahoo Finance sensor (#3943) 2016-10-20 20:14:50 +02:00
henryk 2b37b4251b Fritzbox - Only report a MAC address if it's really there (#3964)
Fixes 'Neither mac or device id passed in'
2016-10-20 20:04:11 +02:00
Fabian Affolter 3aa1b6a3f8 Fix PEP257 issues (#3962) 2016-10-20 19:10:12 +02:00
Pascal Vizeli c32afcd961 Bugfix sonos (#3926)
* Bugfix Sonos

* lint

* Use player uid for looking of exists

* fix lint

* fix unittest

* Change player_id to unique_id
2016-10-20 08:36:48 -07:00
hcooper d60c2d604f Add support for multiple inputs to nmap tracking module as a list (#3944) 2016-10-20 07:15:00 +02:00
David-Leon Pohl 081e61528d Bugfixes for pilight hub component and unit tests (#3948) 2016-10-19 22:02:11 +02:00
Richard Cox 5799d1aec9 Fixing verbage (#3940) 2016-10-18 18:25:47 -07:00
Adam Mills 7d32e5eeeb Logbook filtering of automations by entity_id (#3927)
* Logbook filtering of automations by entity_id

* Trigger action function parameters required
2016-10-18 18:11:35 -07:00
Johann Kellerman 57f32fa629 Fixup device_tracekt.mqtt voluptuous & unit tests (#3904) 2016-10-18 18:10:28 -07:00
Sean Dague 7da47852d4 Yamaha zones (#3920)
* Enhance yamaha component

This enhances the yamaha component to create 1 player per zone,
instead of only one play, which provides direct control of the various
zones for the player.

It also exposes play_media for NET_RADIO sources, which allows direct
setting of that.

This requires code changes in rxv 0.2.0, so the requirement dependency
is raised.

* Support current playback metadata for NET_RADIO

When on NET RADIO, support the currently playing information.
2016-10-18 18:04:15 -07:00
Justin Weberg f1b658ea5d Fix Typo (#3942) 2016-10-18 17:59:14 -07:00
Pascal Vizeli 754e93ff6a Rename add_devices to async_add_devices like dev guide (#3938) 2016-10-19 00:35:22 +02:00
David-Leon Pohl 947c1efca2 New pilight sensor component (#3822)
* Pilight daemon expects JSON serializable data. Thus dict is needed and not a mapping proxy.
* Add explanation why dict as message data is needed
* Use pytest-caplog and no unittest.TestCase
2016-10-18 23:16:20 +02:00
hexa- a1239077d9 Add support for matrix notifications (#3827) 2016-10-18 21:13:00 +02:00
Paulus Schoutsen 53b5dc8e84 Build frontend 2016-10-18 09:10:22 -07:00
Lewis Juggins 09bcd7321a Reset Bravia playing info to ensure state reflects correctly (#3903) 2016-10-17 23:13:35 -07:00
Pascal Vizeli 14ef0ca786 Bugfix Template sensors (#3931) 2016-10-18 07:27:32 +02:00
Rob Capellini 272539105f Replacing tempfile with mock_open in tests (#3753)
- test_bootstrap.py
- test_config.py
- components/test_init.py
- components/test_panel_custom.py
- components/test_api.py
- components/notify/test_file.py
- components/notify/test_demo.py
- components/camera/test_local_file.py
- helpers/test_config_validation.py
- util/test_package.py
- util/test_yaml.py

No changes needed in:
- components/cover/test_command_line.py
- components/switch/test_command_line.py
- components/rollershutter/test_command_line.py
- components/test_shell_command.py
- components/notify/test_command_line.py

Misc changes in:
- components/mqtt/test_server.py

Also, removed some unused mock parameters in tests/components/mqtt/test_server.py.
2016-10-17 20:16:36 -07:00
Fabian Affolter 7d67017de7 Upgrade python-digitalocean to 1.10.0 (#3921) 2016-10-17 20:13:43 -07:00
juggie d921073e77 Ecobee - Celsius (#3906)
* #3899 - Ecobee tempoeratures

* #3899 Remove unused import

* #3899 Implement min/max_temp in ecobee.py as these temperatures have to be in F for ecobee api.

* #3899 Stale print

* #3899 Use min/max_temp from base class

* #3899 Removed unused import (again) so tests pass since changing to use super class

* #3899 Fix long lines

* #3899 Install tox locally... make it happy, commit

* #3899 Remove overridden min/max_temp and instead update __init__:min/max_temp to convert to self.temperature_unit (of the thermostat) as opposed to self.unit_of_measurement (of the system), which is wrong

* Remove unused import from ecobee
2016-10-17 20:06:03 -07:00
Jason Carter 9cf2acb495 Concord232 alarm panel (#3842)
* Adding Concord232 Alarm Panel

* Adding Concord232 Zones as Sensors

* Updating requirements_all.txt

* Adding DOCType and making helper function a closure for clarity

* Fixing D400 error in build

* Fixing pylint errors

* Adding # pylint: disable=too-many-locals

* Updating with proper polling methods

* Fixing Merge issues

* Fixing DocStyle Issue

* Moving Import out of setup

* Fixing DocString

* Removing overthought GLOBAL
2016-10-17 19:59:41 -07:00
Giel Janssens 3b424b034a Netatmo thermostat (#3888)
* Added Netatmo-thermostat

* Remove-CONF_DEVICES
2016-10-17 19:57:02 -07:00
Marcelo Moreira de Mello 76598bc4d2 #3829 - Fixed issued LowHighTuple doesn't define __round__ method for Nest Sensor (#3881)
* #3829 - Fixed type LowHighTuple doesn't define __round__ method issue when Nest is operating in range mode

* Testing if temperature is a tuple instead int or float
2016-10-17 19:55:53 -07:00
William Scanlon c54476b62f Protocol is an int (#3928) 2016-10-17 19:53:50 -07:00
sam-io ae8a8e22ad Support for Apple Push Notification Service (#3756)
* added push notification implementation

* some lint changes

* added docs

* added push notification implementation

* some lint changes

* added docs

* Fixed comment formatting issues

* Added requirments

* Update requirements_all.txt

* Update apns.py

* re-generated requirments_all.txt

* Added link to online docs

* added push notification implementation

* some lint changes

* added docs

* added push notification implementation

* some lint changes

* added docs

* Fixed comment formatting issues

* Added requirments

* Update requirements_all.txt

* Update apns.py

* re-generated requirments_all.txt

* Added link to online docs

* changed to use http/2 library for push notifications

* fixed lint issue

* fixed test that fails on CI

* another go at fixing test that fails on CI

* another go at fixing test that fails on CI

* another go at fixing test that fails on CI

* added missing docstring

* moved service description to main services.yaml file

* renamed apns service
2016-10-17 19:41:49 -07:00
Paulus Schoutsen 4c8d1d9d2f Clean up some async stuff (#3915)
* Clean up some async stuff

* Adjust comments

* Pass hass instance to eventbus
2016-10-17 19:38:41 -07:00
Sean Dague daea93d9f9 Suppress requests/urllib3 connection pool log messages (#3854)
requests/urllib3 is notorious for using the INFO log level for very
DEBUG kinds of information. Given the configurability of python
logging it's actually pretty easy to just set requests to WARN by
default. This cleans out a bunch of largely unuseful log lines from
home assistant output.
2016-10-17 21:14:10 +02:00
Pascal Vizeli 1540bb1279 Async template (#3909)
* port binary_sensor/template

* port sensor/template

* port switch/template

* fix unittest

* fix

* use task instead yield on it

* fix unittest

* fix unittest v2

* fix invalid config

* fix lint

* fix unuset import
2016-10-17 07:00:55 +02:00
Jan Harkes 555e533f67 Added tests for the template is_defined filter 2016-10-17 03:20:07 +02:00
Jan Harkes 118f2f0bad Use a filter to fail rendering undefined variables
Instead of globally using StrictUndefined, introduce a filter that
will trigger a render failure on undefined variables.

Use as `{{ value_json.someval | is_defined }}`.
2016-10-17 03:20:07 +02:00
Jan Harkes c8add59ea5 Fail when rendering undefined objects in Jinja2 templates
By not successfully rendering unknown objects to an empty string
the caller provided error_value actually gets used.

This allows, for instance, the MQTT sensor to retain its state when an
unexpected or unwanted message (#2733/#3834) is received.
2016-10-17 03:20:07 +02:00
Willems Davy 18f5258aaf Emoncms history component (#3531)
* Emoncms_history component, fix git mess

* - switch to track_point_in_time to send all data at foxed interval
- don't use json_dump
- switch to http post instead of http get
2016-10-16 17:05:01 -07:00
Justin Weberg 207c9e8575 Fix Comment (#3913) 2016-10-16 16:56:02 -07:00
Rob Capellini 4891ca1610 Removing calls to mock.assert_called_once_with (#3896)
If a mock's assert_called_once_with method is misspelled (e.g. asert_called_once_with) then the test will appear as passing.  Therefore, this commit removes all instances of assert_called_once_with calls and replaces them with two assertions:

        self.assertEqual(mock.call_count, 1)
        self.assertEqual(mock.call_args, mock.call(call_args))
2016-10-16 16:13:27 -07:00
Lewis Juggins 10c9132046 Resolve issue with delay not passing variables to render (#3901) 2016-10-16 16:08:12 -07:00
Fabian Affolter 71ee847aee Add web scrape sensor (#3841)
* Add web scrape sensor

* Add support for 'value_template', set 'verify_ssl' to true,
and remove 'before', 'after' & 'element'

* Fix pylint issue
2016-10-16 16:06:07 -07:00
Per Sandström d9ae7ceb0c Tellstick switch force update (#3874) 2016-10-16 15:56:55 -07:00
Justin Weberg 2a972b2334 Move micromarkdown to HA (#3908)
* Move micromarkdown to HA

* Fix requests

* Update micromarkdown-js.html& .gz

* Update micromarkdown-js files
2016-10-16 15:47:34 -07:00
Pascal Vizeli 7484152be1 Async speedup add_device callback (#3910)
* Speed up entities processing from add_device callback

* fix lint

* fix bug
2016-10-17 00:35:57 +02:00
Paulus Schoutsen 6581dc2381 Document more core pieces 2016-10-16 13:45:17 -07:00
Paulus Schoutsen 31ec0ac6a7 Add util.async to the dev docs 2016-10-16 13:35:01 -07:00
John Arild Berentsen 8b2edc1514 ZWave: Add association service (#3894)
* Add association service

* Refactor service

* Requested changes

* Grammar in pydocstyle
2016-10-16 20:36:06 +02:00
Adam Mills 2612c6d6b8 Zwave alt delay workaround for HS-WD100+ (#3893) 2016-10-16 20:35:39 +02:00
Pascal Vizeli 0b8b9ecb94 Async EntitiesComponent (#3820)
* first version

* First draft component entities

* Change add_entities to callback from coroutine

* Fix bug add async_prepare_reload

* Group draft v1

* group async

* bugfix

* bugfix v2

* fix lint

* fix extract_entity_ids

* fix other things

* move get_component out of executor

* bugfix

* Address minor changes

* lint

* bugfix - should work now

* make group init async only

* change update handling to old stuff

* fix group handling, remove generator from init

* fix lint

* protect loop for spaming with updates

* fix lint

* update test_group

* fix

* update group handling

* fix __init__ async trouble

* move device_tracker to new layout

* lint

* fix group unittest

* Test with coroutine

* fix bug

* now it works 💯

* ups

* first part of suggestion

* add_entities to coroutine

* change group

* convert add async_add_entity to coroutine

* fix unit tests

* fix lint

* fix lint part 2

* fix wrong import delete

* change async_update_tracked_entity_ids to coroutine

* fix

* revert last change

* fix unittest entity id

* fix unittest

* fix unittest

* fix unittest entity_component

* fix group

* fix group_test

* try part 2 to fix test_group

* fix all entity_component

* rename _process_config

* Change Group to init with factory

* fix lint

* fix lint

* fix callback

* Tweak entity component and group

* More fixes

* Final fixes

* No longer needed blocks

* Address @bbangert comments

* Add test for group.stop

* More callbacks for automation
2016-10-16 09:35:46 -07:00
Fabian Affolter a0fdb2778d Fix name allocation (#3890) 2016-10-16 18:07:34 +02:00
Fabian Affolter 5ef8ca9b03 Upgrade sendgrid to 3.6.0 (#3887) 2016-10-16 09:09:48 +02:00
Fabian Affolter a10fa90357 Update link to docs repo (#3886) 2016-10-15 13:46:45 +02:00
Fabian Affolter 9743e17d62 Minimum/maximum/mean sensor (#3852)
* Add min/max sensor

* Update min_max.py
2016-10-14 21:43:46 -07:00
Marcelo Moreira de Mello 6fcb1b548e Added the ability to Weather Underground to track severe weather alerts (#3505)
*  Added the ability to Weather Underground to track severe weather alerts

*   * Added message on the advisory attr

  * Updated tests

* * Making use of guard clause

* Checking multiple_alerts prior loop

* Using a better way to create dict

* Fixed issue to set to None only the object that failed

* Added unittest

* Split update() method to different calls with their one throttle control to minimize API calls

* Updated unittest and make sure the alert sensor will not return 'unknown' status'

* Removed update() method from state property

* Branch rebased and include Weather Underground attribution

* Update wunderground.py
2016-10-14 21:35:27 -07:00
Adam Mills 1bf5554017 Zwave rgb fix (#3879)
* _zw098 must be set before update_properties

* Fix problematic zwave color light value matching

See https://community.home-assistant.io/t/color-issues-with-aeotec-zwave-zw098/2830
2016-10-14 21:17:48 -07:00
Georgi Kirichkov 49b1643ff0 Relaxes the configuration options for influxdb (#3869)
* Relaxes the configuration options for influxdb

By default influxdb allows unauthenticated access
Home Assistant required at least username and password to be present to properly submit data to influxdb

* Removes unused import of 'copy'

The copy module was used only in the removed test case responsible for testing the missing keys

* Updates InfluxDB config schema to require user and password

Current InfluxDB (v 1.0) can work without any authentication, but when authentication is enabled both username and password should be set.

* Removes extra white space in test_influxdb.py
2016-10-14 21:10:04 -07:00
Michael ce19e6367f Catch MQTT encoding errors (#3749)
* added error handling to mqtt message receive if payload is not utf-8 unicode
added mqtt test for above code as well

* change permission back to 644

* attempting to test new code

* changed exception to AttributeError
fixed test for above

* fixed lint errors I made in tests....mqtt/test_init.py

* more lint fixes for my added test

* remove dual decode of MQTT payload

* convert if to try, except, else statement for mqtt payload decode

* rework mqtt unicode testing code to properly check for log file entriy on unicode decode exception

* fixed lint error

* Update test_init.py
2016-10-14 21:08:44 -07:00
Fabian Affolter 180e146e14 Upgrade uber_rides to 0.2.7 (#3876) 2016-10-14 21:00:27 -07:00
Fabian Affolter bead274b20 Upgrade slacker to 0.9.28 (#3877) 2016-10-14 20:58:49 -07:00
Richard Cox 1cbf8c8049 Zoneminder component (#3795)
* Initial Zoneminder commit

* Fixing bug when ZM sets its function to 'None'

* Adding zoneminder to coverage

* Quick Doc fix

* Update zoneminder.py

Doc Fix

* making the url base optional
2016-10-14 20:56:40 -07:00
Lukas ad259ead50 Add ignore option to zwave customize configuration (#3865) 2016-10-14 08:36:55 -07:00
Daniel Høyer Iversen bb457f47cc Merge pull request #3868 from home-assistant/flux_led_lib
update flux led library
2016-10-14 11:41:18 +02:00
Daniel df3e904fe7 update flux led library 2016-10-14 11:17:03 +02:00
Hugo Dupras 7697cdef0a Pushbullet push an url note if an url is provided inside data (#3758) 2016-10-14 00:11:48 -07:00
Igor Shults 6951b6f60b Allow any positive integer for Z-Wave polling intensity (#3859) 2016-10-14 00:09:52 -07:00
John Arild Berentsen 6ca0d4cd14 Use pass instead of return None (#3856) 2016-10-14 00:09:24 -07:00
Sean Dague a5b756e1e5 Bump proliphix library to 0.4.0 (#3855)
There was a bug in setback setting which is now fixed in 0.4.0. This
ensures that any users have working setback code.
2016-10-14 00:06:53 -07:00
Sean Dague 7848791a04 add arwn sensor platform (#3846)
This adds a sensor component that builds sensors based on the arwn
project (https://github.com/sdague/arwn). This uses a 433mhz receiver
to collect weather data and publish it over mqtt in a well defined
schema, which home-assistant can display as sensors.
2016-10-14 00:06:04 -07:00
Paulus Schoutsen f916fc04f9 Update frontend 2016-10-14 00:02:21 -07:00
John Arild Berentsen 8f4608c654 Use only node id to identify node in set_config_parameter (#3801) 2016-10-13 23:45:00 -07:00
Per Sandström 399a0b470a select next and previous of input select (#3839) 2016-10-13 21:53:47 -07:00
Jan Harkes 4d716cec2b Bump pychromecast dependency to 0.7.6 (#3864) 2016-10-13 21:24:54 -07:00
Lukas 1373db8b60 Include index and instance in object_id of zwave devices (#3759)
* Include index and instance in object_id of zwave devices

* Add the instance id if there is more than one instance for the value
2016-10-13 21:13:05 -07:00
Hydreliox 7771cc2ccf Add Bbox Modem Routeur for device tracker (#3848) 2016-10-13 19:43:51 -07:00
Per Sandström c5ad7996fb Merge pull request #3857 from persandstrom/vsure0.11.1
vsure 0.11.1
2016-10-13 22:18:42 +02:00
Per Sandström 3d94f77998 vsure 0.11.1 2016-10-13 21:53:50 +02:00
Marcelo Moreira de Mello 6330d9cde6 Fixed Fitbit resting heart rate attribute (#3835)
* Fixed Fitbit restingHeartRate field to grab the correct field from dictionary

In [31]: r['activities-heart'][-1].get('value')
Out[31]:
{'customHeartRateZones': [],
 'heartRateZones': [{'caloriesOut': 126.18348,
   'max': 94,
   'min': 30,
   'minutes': 67,
   'name': 'Out of Range'},
  {'caloriesOut': 27.21339,
   'max': 131,
   'min': 94,
   'minutes': 5,
   'name': 'Fat Burn'},
  {'caloriesOut': 0, 'max': 159, 'min': 131, 'minutes': 0, 'name': 'Cardio'},
  {'caloriesOut': 0, 'max': 220, 'min': 159, 'minutes': 0, 'name': 'Peak'}],
 'restingHeartRate': 69}

In [32]: r['activities-heart'][-1].get('value').get('restingHeartRate')
Out[32]: 69

* Renamed sensor to Resting Heart Rate to match it

* Fixed lint style
2016-10-13 09:22:05 -07:00
Hugo Dupras 9a0bb62654 Hotfix for Netatmo discovery (#3837)
This should definetly fix #2601

Signed-off-by: Hugo D. (jabesq) <jabesq@gmail.com>
2016-10-13 09:21:22 -07:00
Fabian Affolter d873a7baf0 Use async_render_* and fix config validation (#3847) 2016-10-13 09:20:49 -07:00
Robbie Trencheny c663d85129 Add Alexa Flash Briefing Skill API support (#3745)
* Add Alexa Flash Briefing Skill API support

* Set default value for text to empty string as per API docs

* Clean up existing Alexa tests

* Update configuration parsing and validation

* Add tests for the Flash Briefing API

* Update test_alexa.py
2016-10-13 09:14:22 -07:00
John 8c13d3ed4c Update zwave lights to increase update delay timer as needed (#3741)
Fix permissions
2016-10-13 09:09:41 -07:00
Johann Kellerman cb322f72db Add persistent notifications to bootstrap (#3738)
* Add persistent notifications to bootstrap

* Rebase, Fix test
2016-10-13 09:09:07 -07:00
Scott Reston 39a446c43c Proper title, added album and artist for Squeezebox (#3735)
* Proper title, added album and artist

Title had previously concatenated artist - title.

* Made changes suggested by @balloobbot
2016-10-13 09:07:10 -07:00
wokar aa8622f8e8 Added include and exclude functionality to history component (#3674)
* added include and exclude functionality to history component

* fixed summary lines in test method doc.

* cleanup of query filter creation

* o improved config validation
o move move IGNORE_DOMAINS to Filter.apply()
o removed config from Last5StatesView
o Filters instance is now created on setup
o config values are processed in setup and set to the Filters instance
o function _set_filters_in_query() moved to Filters class and renamed to apply()

* added unittests for more include/exclude filter combinations

* make pylint happy
2016-10-13 08:54:45 -07:00
Vittorio Monaco e031b8078f Fixes an issue where Chromecast audio groups were not properly discovered (#3630)
* Fixes an issue where Chromecast audio groups were not properly discovered

* Forgot to commit the main fix

* Removes unused variable

* Doesn't use a protected API anymore

* PR remarks

* Fixes tests, adds comment

* Restores line as it was in the original commit, rephrases comment

* Should fix lint issues

* Trailing whitespace

* Some more lint
2016-10-13 08:51:43 -07:00
jgriff2 e1647fb6ac Add synology ss cameras (#3626)
* Add files via upload

* Update .coveragerc

* test

* Update synology camera

* Use voluptuous for synology

* Use voluptuous for synology

* Use voluptuous for synology

* Use voluptuous for synology

* Conform synology to flake8

* Added Whitelist to synology

* Sync to dev branch

* Added helper function to synology
2016-10-13 08:49:58 -07:00
Lewis Juggins 10feac11d9 Support recursive config inclusions (#3783) 2016-10-12 12:05:41 +02:00
Paulus Schoutsen d4e2332ce0 Merge pull request #3814 from home-assistant/release-0-30-1
0.30.2
2016-10-11 22:28:00 -07:00
Jean Regisser 1d7169403b Add again certifi to Docker image (#3813)
Latest versions of certifi (>= 2016.8.31) don't seem to break anything
anymore.
See home-assistant/home-assistant#2554
2016-10-11 22:25:17 -07:00
Keith Lamprecht e4685de459 Restore Optional Target Config Attribute (notify.pushover) (#3769)
* Restore Optional Target Config Attribute

* Fix Tabs

* Change indents to spaces

* Make a target fix

* Change to simpler not syntax
2016-10-11 21:59:45 -07:00
Keith Lamprecht d83de36c32 Restore Optional Target Config Attribute (notify.pushover) (#3769)
* Restore Optional Target Config Attribute

* Fix Tabs

* Change indents to spaces

* Make a target fix

* Change to simpler not syntax
2016-10-11 21:59:34 -07:00
Fabian Affolter 73547c8c4b Fix slack targets (#3826) 2016-10-11 21:58:54 -07:00
Fabian Affolter 40094cecae Fix slack targets (#3826) 2016-10-11 21:16:11 -07:00
Robbie Trencheny 0b327cd4d9 Notify: Only attach target if in call data (#3831)
* Only pass through the target if it has a value

* Target will no longer be none
2016-10-11 20:34:14 -07:00
Robbie Trencheny d302dbec2d Notify: Only attach target if in call data (#3831)
* Only pass through the target if it has a value

* Target will no longer be none
2016-10-11 20:33:41 -07:00
Fabian Affolter e135691bd6 Migrate to voluptuous (#3817) 2016-10-11 08:33:22 -07:00
Fabian Affolter d4dc2707a1 Use voluptuous for eQ-3 thermostat (#3729)
* Migrate to voluptuous

* Fix requirement and typo
2016-10-11 11:27:31 +02:00
Paulus Schoutsen a8cdf36d5c Update recorder callback (#3812) 2016-10-11 00:58:43 -07:00
Fabian Affolter a99f36f519 Migrate to voluptuous (#3737) 2016-10-11 00:56:57 -07:00
Fabian Affolter 0568ef025b Use voluptuous for Heatmiser (#3732) 2016-10-11 00:53:24 -07:00
Fabian Affolter 8ded8f572a Add/adjust attribution of sensor platform (#3719)
* Add/adjust attribution

* Fix typo
2016-10-11 00:28:19 -07:00
Fabian Affolter 7cf2c48175 Use voluptuous for FitBit (#3686)
* Migrate to voluptuous

* Fix default
2016-10-11 00:27:15 -07:00
Fabian Affolter 7cf9ff83bc Migrate to voluptuous (#3293) [BREAKING CHANGE] 2016-10-11 00:26:11 -07:00
Paulus Schoutsen dfe9af7110 Version bump to 0.30.2 2016-10-11 00:21:01 -07:00
Erik Eriksson 016e8f833d slugify (#3777) 2016-10-11 00:20:49 -07:00
Teemu Mikkonen 574df0f420 Fix for html5 notification tag problem. Fixes #3774 (#3790) 2016-10-11 00:20:49 -07:00
Pascal Vizeli f18f181962 Hotfix device name with autodiscovery (#3791) 2016-10-11 00:20:49 -07:00
Ellis Percival 4754455295 Make 'pin' optional for zigbee device config. (#3799) 2016-10-11 00:20:49 -07:00
Robbie Trencheny 8c1317f278 Wrap found target in list (#3809)
* Wrap found target in list

* Fix test_messages_to_targets_route
2016-10-11 00:20:49 -07:00
Russell Cloran 7c2cb6cffd Separate climate platform and presentation units (#3755)
* Separate platform and presentation units in climate

* Fix unit tests

Maybe

* Fix unit tests some more

Maybe

* Rename _platform_unit_of_measurement to temperature_unit

* Fix tests for renamed attribute
2016-10-11 00:00:29 -07:00
Ferry van Zeelst 8c9d1d9af1 Added additional checks which hides functions which are not supported by Nest (#3751)
* Added additional checks which hides functions which are not support (like fans / humidity / cooling)

* Fixed pylint and flake8 errors (not test file available)

* Fixed pydocstyle error

* Refactored Code and Comments as described in pull-request

* Added additional comment and requesting retest

* Upgraded to python-nest 2.11 which contains previously hidden functions
2016-10-10 23:57:14 -07:00
Lukas 941fccd3fc Update python-lirc to 1.2.3 (#3784)
* Upgrade to latest python-lirc 1.2.3

* update requirements_all.txt
2016-10-10 23:44:41 -07:00
Clemens Wolff 711526e574 Cache condition in helpers.Script (#3797)
* Cache condition in helpers.Script

The caching is a simple in-memory, per-instance dictionary.
We use __str__ to format cache keys.

This naive implementation has some disadvantages (e.g., we won't be able
to cache two conditions that contain references to
distinct-but-equivalent object instances and we don't have any control
over the size of the condition cache), but for most simple use-cases the
approach should be good enough.

Resolves #3629

* Fix docstring style
2016-10-10 23:36:38 -07:00
Fabian Affolter a2503e4d13 Upgrade sqlalchemy to 1.1.1 (#3796) 2016-10-10 23:29:43 -07:00
Fabian Affolter 656ee52435 Upgrade cherrypy to 8.1.2 (#3805) 2016-10-10 23:27:53 -07:00
Fabian Affolter 002660fd9e Upgrade dnspython3 to 1.15.0 (#3804) 2016-10-10 23:24:10 -07:00
Fabian Affolter 63580f9e03 Upgrade pyowm to 2.5.0 (#3806) 2016-10-10 23:23:32 -07:00
Teemu Mikkonen 87d9cdd78f Fix for html5 notification tag problem. Fixes #3774 (#3790) 2016-10-10 23:17:27 -07:00
phardy c8ca66b671 Stop GTFS component from overwriting friendly_name (#3798)
* Prevent update from clobbering configured name.

* linted

* Fixing awkward line breaking.
2016-10-10 22:36:20 -07:00
Robbie Trencheny 9b98d470c2 Wrap found target in list (#3809)
* Wrap found target in list

* Fix test_messages_to_targets_route
2016-10-10 22:31:15 -07:00
sander76 b19ec21e88 Changing import as powerview api did change. (#3780) 2016-10-10 22:30:27 -07:00
Fabian Affolter 3b331eac56 Update docstrings (sensor.pi_hole, sensor.haveibeenpwned) (#3793)
* Fix docstrings

* Update docstrings
2016-10-10 19:38:32 +02:00
Fabian Affolter 552265bc31 No longer use old name (#3792) 2016-10-10 19:38:20 +02:00
Ellis Percival df58f718ab Make 'pin' optional for zigbee device config. (#3799) 2016-10-10 16:53:18 +02:00
Pascal Vizeli 76a1a54369 Hotfix device name with autodiscovery (#3791) 2016-10-10 13:46:23 +02:00
Per Sandström b6e008be71 Merge pull request #3782 from persandstrom/vsure0.11.0
vsure0.11.0
2016-10-09 22:01:46 +02:00
Per Sandström 9d5c20b629 vsure0.11.0 2016-10-09 21:47:35 +02:00
Erik Eriksson 63d9ea6643 slugify (#3777) 2016-10-09 09:15:58 -07:00
Hugo Dupras 1d0df63615 Netatmo binary sensor (#3455)
* Basic support for Netatmo welcome binary sensors

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

* Bug fixes and optimization for Netatmo devices

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

* pylint fixes

* Bug Fixing and optimization for Netatmo devices

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

* Add unique_id for cameras to avoid duplicate
And global config to disable discovery for netatmo devices

Signed-off-by: Hugo D. (jabesq) <jabesq@gmail.com>
2016-10-09 08:45:12 -07:00
hexa- 154eacef6c Http: Change approved_ips from string to cidr validation (#3532) [BREAKING CHANGE]
* Change approved_ips from string to cidr validation

Relabel to trusted_networks, better reflecting its expected inputs,
everything that ipaddress.ip_networks recognizes as an ip network
is possible:
- 127.0.0.1      (single ipv4 addresses)
- 192.168.0.0/24 (ipv4 networks)
- ::1            (single ipv6 addresses)
- 2001:DB8::/48  (ipv6 networks)

* Add support for the X-Forwarded-For header
2016-10-09 08:13:30 -07:00
Fabian Affolter dc95b28487 Use voluptuous for Russound RNET (#3689)
* Migrate to voluptuous

* Remove wrong default
2016-10-09 16:40:54 +02:00
Paulus Schoutsen c9a88322d6 Merge pull request #3765 from home-assistant/hotfix-0-30-1
0.30.1
2016-10-08 14:50:52 -07:00
Paulus Schoutsen 46b58db2ff Version bump to 0.30.1 2016-10-08 14:42:47 -07:00
mtl010957 78cbfa3f96 Fixed issue #3760, handle X10 unit numbers greater than 9. (#3763) 2016-10-08 14:42:24 -07:00
Roi Dayan dba5c74c8f Fix command line cover template (#3754)
The command line cover value template is optional so we
need to check it's not none before assigning hass to it.

Fixes #3649

Signed-off-by: Roi Dayan <roi.dayan@gmail.com>
2016-10-08 14:42:24 -07:00
Johann Kellerman a94a5ac9b5 Coerce device IDs from known_devices to be slugs (#3764)
* Slugify & consider_home test fix [due to load valid PR]

* undo schema change

* Fix slugify error
2016-10-08 14:42:24 -07:00
Johann Kellerman 4d9bac6f9c Coerce device IDs from known_devices to be slugs (#3764)
* Slugify & consider_home test fix [due to load valid PR]

* undo schema change

* Fix slugify error
2016-10-08 14:40:50 -07:00
Roi Dayan 6419d273ea Fix command line cover template (#3754)
The command line cover value template is optional so we
need to check it's not none before assigning hass to it.

Fixes #3649

Signed-off-by: Roi Dayan <roi.dayan@gmail.com>
2016-10-08 13:03:32 -07:00
mtl010957 7882b19dc5 Fixed issue #3760, handle X10 unit numbers greater than 9. (#3763) 2016-10-08 12:57:40 -07:00
Willems Davy 8f8bba4ad7 Haveibeenpwned sensor platform (#3618)
* Initial version of "haveibeenpwned" sensor component

* 2 flake8 fixes

* remove debugging error message

* Increase scan_interval as well as throttle to make sure that during initial startup of hass the request happens with 5 seconds delays and after startup with 15 minutes delays. Scan_interval is increased also to not call update as often

* update .coveragerc

* remove (ssl) verify=False

* - use dict to keep the request values with email as key
- use track_point_in_time system to make sure data updates initially at 5 seconds between each call until all sensor's email have a result in the dict.

* fix a pylint error that happend on the py35 tests
2016-10-08 11:38:58 -07:00
Johann Kellerman 7b40a641ec Continue on invalid platforms and new setup_component unit tests (#3736) 2016-10-08 11:27:35 -07:00
wokar 1b26b5ad14 add include configuration to logbook (#3739) 2016-10-08 11:26:14 -07:00
Hugo Dupras bbbb4441ea Add discovery support for Netatmo weather Station (#3714)
* Add discovery support for Netatmo Weather station

Only The weather information are currently supported (No battery or wifi status supported)

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

* Add unique_id for netatmo sensors to avoid duplicate

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

* Update requirements_all.txt and PEP8 fixes

Signed-off-by: Hugo D. (jabesq) <jabesq@gmail.com>
2016-10-08 11:26:01 -07:00
Paulus Schoutsen 1e2e877302 Version bump to 0.31.0.dev0 2016-10-08 09:58:28 -07:00
646 changed files with 28553 additions and 14330 deletions
+47 -15
View File
@@ -28,12 +28,21 @@ 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
homeassistant/components/ios.py
homeassistant/components/*/ios.py
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/litejet.py
homeassistant/components/*/litejet.py
homeassistant/components/modbus.py
homeassistant/components/*/modbus.py
@@ -92,34 +101,44 @@ 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
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
homeassistant/components/knx.py
homeassistant/components/*/knx.py
homeassistant/components/ffmpeg.py
homeassistant/components/*/ffmpeg.py
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/concord232.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py
homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/heatmiser.py
homeassistant/components/climate/homematic.py
homeassistant/components/climate/knx.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py
@@ -127,16 +146,18 @@ omit =
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bbox.py
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/ddwrt.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
@@ -144,11 +165,10 @@ omit =
homeassistant/components/device_tracker/volvooncall.py
homeassistant/components/discovery.py
homeassistant/components/downloader.py
homeassistant/components/emoncms_history.py
homeassistant/components/fan/mqtt.py
homeassistant/components/feedreader.py
homeassistant/components/foursquare.py
homeassistant/components/garage_door/rpi_gpio.py
homeassistant/components/garage_door/wink.py
homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py
homeassistant/components/joaoapps_join.py
@@ -162,12 +182,14 @@ omit =
homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py
homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py
homeassistant/components/lirc.py
homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/emby.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/itunes.py
@@ -178,6 +200,7 @@ omit =
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/philips_js.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
@@ -197,7 +220,9 @@ omit =
homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/kodi.py
homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/nfandroidtv.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py
@@ -209,6 +234,7 @@ omit =
homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py
homeassistant/components/notify/telstra.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twitter.py
homeassistant/components/notify/xmpp.py
@@ -216,13 +242,18 @@ omit =
homeassistant/components/openalpr.py
homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/bom.py
homeassistant/components/sensor/coinmarketcap.py
homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/cups.py
homeassistant/components/sensor/currencylayer.py
homeassistant/components/sensor/darksky.py
homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
@@ -235,9 +266,12 @@ omit =
homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gpsd.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hddtemp.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py
@@ -252,8 +286,9 @@ omit =
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/pi_hole.py
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/speedtest.py
@@ -261,6 +296,7 @@ omit =
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/synologydsm.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/ted5000.py
homeassistant/components/sensor/temper.py
@@ -270,9 +306,7 @@ omit =
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/worldclock.py
homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yahoo_finance.py
homeassistant/components/sensor/yweather.py
homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.py
@@ -283,18 +317,16 @@ omit =
homeassistant/components/switch/mystrom.py
homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pilight.py
homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py
homeassistant/components/thermostat/eq3btsmart.py
homeassistant/components/thermostat/heatmiser.py
homeassistant/components/thermostat/homematic.py
homeassistant/components/thermostat/proliphix.py
homeassistant/components/thermostat/radiotherm.py
homeassistant/components/thingspeak.py
homeassistant/components/upnp.py
homeassistant/components/weather/openweathermap.py
homeassistant/components/zeroconf.py
+3 -3
View File
@@ -1,9 +1,9 @@
**Description:**
**Related issue (if applicable):** fixes #
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
**Example entry for `configuration.yaml` (if applicable):**
```yaml
@@ -13,7 +13,7 @@
**Checklist:**
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
If the code communicates with devices, web services, or third-party tools:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
+2
View File
@@ -0,0 +1,2 @@
python:
enabled: true
+3 -3
View File
@@ -2,11 +2,11 @@ sudo: false
matrix:
fast_finish: true
include:
- python: "3.4"
- python: "3.4.2"
env: TOXENV=py34
- python: "3.4"
- python: "3.4.2"
env: TOXENV=requirements
- python: "3.5"
- python: "3.4.2"
env: TOXENV=lint
- python: "3.5"
env: TOXENV=typing
+1 -2
View File
@@ -19,8 +19,7 @@ RUN script/build_python_openzwave && \
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
COPY requirements_all.txt requirements_all.txt
# certifi breaks Debian based installs
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
pip3 install mysqlclient psycopg2 uvloop
# Copy source
+22 -58
View File
@@ -1,7 +1,7 @@
homeassistant:
# Omitted values in this section will be auto detected using freegeoip.io
# Location required to calculate the time the sun rises and sets.
# Location required to calculate the time the sun rises and sets.
# Coordinates are also used for location for weather related components.
# Google Maps can be used to determine more precise GPS coordinates.
latitude: 32.87336
@@ -43,12 +43,10 @@ device_tracker:
username: admin
password: PASSWORD
chromecast:
switch:
platform: wemo
thermostat:
climate:
platform: nest
# Required: username and password that are used to login to the Nest thermostat.
username: myemail@mydomain.com
@@ -79,7 +77,6 @@ group:
entities:
- group.awesome_people
- group.climate
kitchen:
name: Kitchen
entities:
@@ -92,52 +89,23 @@ group:
- input_boolean.notify_home
- camera.demo_camera
example:
simple_alarm:
# Which light/light group has to flash when a known device comes home
known_light: light.Bowl
# Which light/light group has to flash red when light turns on while no one home
unknown_light: group.living_room
browser:
keyboard:
# https://home-assistant.io/getting-started/automation/
automation:
- alias: 'Rule 1 Light on in the evening'
trigger:
- platform: sun
- alias: Turn on light when sun sets
trigger:
platform: sun
event: sunset
offset: "-01:00:00"
- platform: state
condition:
condition: state
entity_id: group.all_devices
state: home
condition:
- platform: state
entity_id: group.all_devices
state: home
- platform: time
after: "16:00:00"
before: "23:00:00"
action:
service: homeassistant.turn_on
entity_id: group.living_room
state: 'home'
action:
service: light.turn_on
- alias: 'Rule 2 - Away Mode'
trigger:
- platform: state
entity_id: group.all_devices
state: 'not_home'
condition: use_trigger_values
action:
service: light.turn_off
entity_id: group.all_lights
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
# Each sensor label should be unique or your sensors might not load correctly.
# Another way to do is to collect all entries under one "sensor:"
# sensor:
# - platform: mqtt
@@ -154,34 +122,30 @@ sensor:
arg: '/'
- type: 'disk_use_percent'
arg: '/home'
- type: 'disk_use'
arg: '/home'
sensor 2:
platform: forecast
api_key: <register on Forecast.io for your PRIVATE API>
monitored_conditions:
- summary
- precip_type
- precip_intensity
- temperature
platform: cpuspeed
script:
# Turns on the bedroom lights and then the living room lights 1 minute later
wakeup:
alias: Wake Up
sequence:
# alias is optional
- event: LOGBOOK_ENTRY
event_data:
name: Paulus
message: is waking up
entity_id: device_tracker.paulus
domain: light
- alias: Bedroom lights on
execute_service: light.turn_on
service_data:
service: light.turn_on
data:
entity_id: group.bedroom
brightness: 100
- delay:
# supports seconds, milliseconds, minutes, hours, etc.
minutes: 1
- alias: Living room lights on
execute_service: light.turn_on
service_data:
service: light.turn_on
data:
entity_id: group.living_room
scene:
+20
View File
@@ -8,11 +8,31 @@
.. autoclass:: Config
:members:
.. autoclass:: Event
:members:
.. autoclass:: EventBus
:members:
.. autoclass:: HomeAssistant
:members:
.. autoclass:: State
:members:
.. autoclass:: StateMachine
:members:
.. autoclass:: ServiceCall
:members:
.. autoclass:: ServiceRegistry
:members:
Module contents
---------------
.. automodule:: homeassistant.core
:members:
:undoc-members:
:show-inheritance:
+8
View File
@@ -4,6 +4,14 @@ homeassistant.util package
Submodules
----------
homeassistant.util.async module
-------------------------------
.. automodule:: homeassistant.util.async
:members:
:undoc-members:
:show-inheritance:
homeassistant.util.color module
-------------------------------
+50 -12
View File
@@ -2,7 +2,7 @@ swagger: '2.0'
info:
title: Home Assistant
description: Home Assistant REST API
version: "1.0.0"
version: "1.0.1"
# the domain of the service
host: localhost:8123
@@ -12,17 +12,17 @@ schemes:
- https
securityDefinitions:
api_key:
type: apiKey
description: API password
name: api_password
in: query
# api_key:
#api_key:
# type: apiKey
# description: API password
# name: x-ha-access
# in: header
# name: api_password
# in: query
api_key:
type: apiKey
description: API password
name: x-ha-access
in: header
# will be prefixed to all paths
basePath: /api
@@ -38,6 +38,8 @@ paths:
description: Returns message if API is up and running.
tags:
- Core
security:
- api_key: []
responses:
200:
description: API is up and running
@@ -53,6 +55,8 @@ paths:
description: Returns the current configuration as JSON.
tags:
- Core
security:
- api_key: []
responses:
200:
description: Current configuration
@@ -81,6 +85,8 @@ paths:
summary: Returns all data needed to bootstrap Home Assistant.
tags:
- Core
security:
- api_key: []
responses:
200:
description: Bootstrap information
@@ -96,6 +102,8 @@ paths:
description: Returns an array of event objects. Each event object contain event name and listener count.
tags:
- Events
security:
- api_key: []
responses:
200:
description: Events
@@ -113,6 +121,8 @@ paths:
description: Returns an array of service objects. Each object contains the domain and which services it contains.
tags:
- Services
security:
- api_key: []
responses:
200:
description: Services
@@ -130,6 +140,8 @@ paths:
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
tags:
- State
security:
- api_key: []
responses:
200:
description: State changes
@@ -148,6 +160,8 @@ paths:
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
tags:
- State
security:
- api_key: []
responses:
200:
description: States
@@ -166,6 +180,8 @@ paths:
Returns a state object for specified entity_id.
tags:
- State
security:
- api_key: []
parameters:
- name: entity_id
in: path
@@ -223,6 +239,8 @@ paths:
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
tags:
- Core
security:
- api_key: []
produces:
- text/plain
responses:
@@ -239,6 +257,8 @@ paths:
Returns the data (image) from the specified camera entity_id.
tags:
- Camera
security:
- api_key: []
produces:
- image/jpeg
parameters:
@@ -262,6 +282,8 @@ paths:
Fires an event with event_type
tags:
- Events
security:
- api_key: []
consumes:
- application/json
parameters:
@@ -286,6 +308,8 @@ paths:
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
tags:
- Services
security:
- api_key: []
consumes:
- application/json
parameters:
@@ -317,6 +341,8 @@ paths:
Render a Home Assistant template.
tags:
- Template
security:
- api_key: []
consumes:
- application/json
produces:
@@ -338,6 +364,8 @@ paths:
Setup event forwarding to another Home Assistant instance.
tags:
- Core
security:
- api_key: []
consumes:
- application/json
parameters:
@@ -376,6 +404,8 @@ paths:
tags:
- Core
- Events
security:
- api_key: []
produces:
- text/event-stream
parameters:
@@ -420,8 +450,16 @@ definitions:
location_name:
type: string
unit_system:
type: string
description: The system for measurement units
type: object
properties:
length:
type: string
mass:
type: string
temperature:
type: string
volume:
type: string
time_zone:
type: string
version:
+8 -3
View File
@@ -14,6 +14,7 @@ from homeassistant.const import (
__version__,
EVENT_HOMEASSISTANT_START,
REQUIRED_PYTHON_VER,
REQUIRED_PYTHON_VER_WIN,
RESTART_EXIT_CODE,
)
from homeassistant.util.async import run_callback_threadsafe
@@ -44,8 +45,7 @@ def monkey_patch_asyncio():
See https://bugs.python.org/issue26617 for details of the Python
bug.
"""
# pylint: disable=no-self-use, too-few-public-methods, protected-access
# pylint: disable=bare-except
# pylint: disable=no-self-use, protected-access, bare-except
import asyncio.tasks
class IgnoreCalls:
@@ -64,7 +64,12 @@ def monkey_patch_asyncio():
def validate_python() -> None:
"""Validate we're running the right Python version."""
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
if sys.platform == "win32" and \
sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN:
print("Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER_WIN))
sys.exit(1)
elif sys.version_info[:3] < REQUIRED_PYTHON_VER:
print("Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER))
sys.exit(1)
+289 -98
View File
@@ -1,11 +1,10 @@
"""Provides methods to bootstrap a home assistant instance."""
import asyncio
import logging
import logging.handlers
import os
import sys
from collections import defaultdict
from threading import RLock
from collections import OrderedDict
from types import ModuleType
from typing import Any, Optional, Dict
@@ -19,6 +18,8 @@ import homeassistant.config as conf_util
import homeassistant.core as core
import homeassistant.loader as loader
import homeassistant.util.package as pkg_util
from homeassistant.util.async import (
run_coroutine_threadsafe, run_callback_threadsafe)
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
from homeassistant.exceptions import HomeAssistantError
@@ -26,33 +27,50 @@ from homeassistant.helpers import (
event_decorators, service, config_per_platform, extract_domain_configs)
_LOGGER = logging.getLogger(__name__)
_SETUP_LOCK = RLock()
_CURRENT_SETUP = []
ATTR_COMPONENT = 'component'
ERROR_LOG_FILENAME = 'home-assistant.log'
_PERSISTENT_ERRORS = {}
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
def setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies."""
return run_coroutine_threadsafe(
async_setup_component(hass, domain, config), loop=hass.loop).result()
@asyncio.coroutine
def async_setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies.
This method is a coroutine.
"""
if domain in hass.config.components:
_LOGGER.debug('Component %s already set up.', domain)
return True
_ensure_loader_prepared(hass)
if not loader.PREPARED:
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)
# OrderedSet is empty if component or dependencies could not be resolved
if not components:
_async_persistent_notification(hass, domain, True)
return False
for component in components:
if not _setup_component(hass, component, config):
res = yield from _async_setup_component(hass, component, config)
if not res:
_LOGGER.error('Component %s failed to setup', component)
_async_persistent_notification(hass, component, True)
return False
return True
@@ -60,7 +78,10 @@ def setup_component(hass: core.HomeAssistant, domain: str,
def _handle_requirements(hass: core.HomeAssistant, component,
name: str) -> bool:
"""Install the requirements for a component."""
"""Install the requirements for a component.
This method needs to run in an executor.
"""
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
return True
@@ -68,70 +89,109 @@ def _handle_requirements(hass: core.HomeAssistant, component,
if not pkg_util.install_package(req, target=hass.config.path('deps')):
_LOGGER.error('Not initializing %s because could not install '
'dependency %s', name, req)
_async_persistent_notification(hass, name)
return False
return True
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
"""Setup a component for Home Assistant."""
# pylint: disable=too-many-return-statements,too-many-branches
# pylint: disable=too-many-statements
@asyncio.coroutine
def _async_setup_component(hass: core.HomeAssistant,
domain: str, config) -> bool:
"""Setup a component for Home Assistant.
This method is a coroutine.
"""
# pylint: disable=too-many-return-statements
if domain in hass.config.components:
return True
with _SETUP_LOCK:
# It might have been loaded while waiting for lock
if domain in hass.config.components:
return True
setup_lock = hass.data.get('setup_lock')
if setup_lock is None:
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
if domain in _CURRENT_SETUP:
_LOGGER.error('Attempt made to setup %s during setup of %s',
domain, domain)
return False
setup_progress = hass.data.get('setup_progress')
if setup_progress is None:
setup_progress = hass.data['setup_progress'] = []
config = prepare_setup_component(hass, config, domain)
if domain in setup_progress:
_LOGGER.error('Attempt made to setup %s during setup of %s',
domain, domain)
_async_persistent_notification(hass, domain, True)
return False
try:
# Used to indicate to discovery that a setup is ongoing and allow it
# to wait till it is done.
did_lock = False
if not setup_lock.locked():
yield from setup_lock.acquire()
did_lock = True
setup_progress.append(domain)
config = yield from async_prepare_setup_component(hass, config, domain)
if config is None:
return False
component = loader.get_component(domain)
_CURRENT_SETUP.append(domain)
if component is None:
_async_persistent_notification(hass, domain)
return False
async_comp = hasattr(component, 'async_setup')
try:
result = component.setup(hass, config)
if result is False:
_LOGGER.error('component %s failed to initialize', domain)
return False
elif result is not True:
_LOGGER.error('component %s did not return boolean if setup '
'was successful. Disabling component.', domain)
loader.set_component(domain, None)
return False
_LOGGER.info("Setting up %s", domain)
if async_comp:
result = yield from component.async_setup(hass, config)
else:
result = yield from hass.loop.run_in_executor(
None, component.setup, hass, config)
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error during setup of component %s', domain)
_async_persistent_notification(hass, domain, True)
return False
if result is False:
_LOGGER.error('component %s failed to initialize', domain)
_async_persistent_notification(hass, domain, True)
return False
elif result is not True:
_LOGGER.error('component %s did not return boolean if setup '
'was successful. Disabling component.', domain)
_async_persistent_notification(hass, domain, True)
loader.set_component(domain, None)
return False
finally:
_CURRENT_SETUP.remove(domain)
hass.config.components.append(component.DOMAIN)
# Assumption: if a component does not depend on groups
# it communicates with devices
if 'group' not in getattr(component, 'DEPENDENCIES', []) and \
hass.pool.worker_count <= 10:
hass.pool.add_worker()
hass.bus.fire(
hass.bus.async_fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
)
return True
finally:
setup_progress.remove(domain)
if did_lock:
setup_lock.release()
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
domain: str):
"""Prepare setup of a component and return processed config."""
return run_coroutine_threadsafe(
async_prepare_setup_component(hass, config, domain), loop=hass.loop
).result()
@asyncio.coroutine
def async_prepare_setup_component(hass: core.HomeAssistant, config: dict,
domain: str):
"""Prepare setup of a component and return processed config.
This method is a coroutine.
"""
# pylint: disable=too-many-return-statements
component = loader.get_component(domain)
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
@@ -147,7 +207,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
try:
config = component.CONFIG_SCHEMA(config)
except vol.Invalid as ex:
log_exception(ex, domain, config)
async_log_exception(ex, domain, config, hass)
return None
elif hasattr(component, 'PLATFORM_SCHEMA'):
@@ -157,8 +217,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.Invalid as ex:
log_exception(ex, domain, config)
return None
async_log_exception(ex, domain, config, hass)
continue
# Not all platform components follow same pattern for platforms
# So if p_name is None we are not going to validate platform
@@ -167,20 +227,21 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
platforms.append(p_validated)
continue
platform = prepare_setup_platform(hass, config, domain,
p_name)
platform = yield from async_prepare_setup_platform(
hass, config, domain, p_name)
if platform is None:
return None
continue
# Validate platform specific schema
if hasattr(platform, 'PLATFORM_SCHEMA'):
try:
# pylint: disable=no-member
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.Invalid as ex:
log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated)
return None
async_log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated, hass)
continue
platforms.append(p_validated)
@@ -191,7 +252,9 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
if key not in filter_keys}
config[domain] = platforms
if not _handle_requirements(hass, component, domain):
res = yield from hass.loop.run_in_executor(
None, _handle_requirements, hass, component, domain)
if not res:
return None
return config
@@ -200,7 +263,22 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) -> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup."""
_ensure_loader_prepared(hass)
return run_coroutine_threadsafe(
async_prepare_setup_platform(hass, config, domain, platform_name),
loop=hass.loop
).result()
@asyncio.coroutine
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) \
-> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup.
This method is a coroutine.
"""
if not loader.PREPARED:
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
@@ -209,6 +287,7 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
# Not found
if platform is None:
_LOGGER.error('Unable to find platform %s', platform_path)
_async_persistent_notification(hass, platform_path)
return None
# Already loaded
@@ -217,20 +296,23 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
# Load dependencies
for component in getattr(platform, 'DEPENDENCIES', []):
if not setup_component(hass, component, config):
res = yield from async_setup_component(hass, component, config)
if not res:
_LOGGER.error(
'Unable to prepare setup for platform %s because '
'dependency %s could not be initialized', platform_path,
component)
_async_persistent_notification(hass, platform_path, True)
return None
if not _handle_requirements(hass, platform, platform_path):
res = yield from hass.loop.run_in_executor(
None, _handle_requirements, hass, platform, platform_path)
if not res:
return None
return platform
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config: Dict[str, Any],
hass: Optional[core.HomeAssistant]=None,
config_dir: Optional[str]=None,
@@ -250,15 +332,55 @@ def from_config_dict(config: Dict[str, Any],
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
@asyncio.coroutine
def _async_init_from_config_dict(future):
try:
re_hass = yield from async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days)
future.set_result(re_hass)
# pylint: disable=broad-except
except Exception as exc:
future.set_exception(exc)
# run task
future = asyncio.Future(loop=hass.loop)
hass.async_add_job(_async_init_from_config_dict(future))
hass.loop.run_until_complete(future)
return future.result()
@asyncio.coroutine
def async_from_config_dict(config: Dict[str, Any],
hass: core.HomeAssistant,
config_dir: Optional[str]=None,
enable_log: bool=True,
verbose: bool=False,
skip_pip: bool=False,
log_rotate_days: Any=None) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a config dict.
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:
conf_util.process_ha_core_config(hass, core_config)
yield from conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as ex:
log_exception(ex, 'homeassistant', core_config)
async_log_exception(ex, 'homeassistant', core_config, hass)
return None
conf_util.process_ha_config_upgrade(hass)
yield from hass.loop.run_in_executor(
None, conf_util.process_ha_config_upgrade, hass)
if enable_log:
enable_logging(hass, verbose, log_rotate_days)
@@ -268,41 +390,43 @@ def from_config_dict(config: Dict[str, Any],
_LOGGER.warning('Skipping pip installation of required modules. '
'This may cause issues.')
_ensure_loader_prepared(hass)
if not loader.PREPARED:
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()
if key != core.DOMAIN)
# Setup in a thread to avoid blocking
def component_setup():
"""Set up a component."""
if not core_components.setup(hass, config):
_LOGGER.error('Home Assistant core failed to initialize. '
'Further initialization aborted.')
return hass
# setup components
# pylint: disable=not-an-iterable
res = yield from core_components.async_setup(hass, config)
if not res:
_LOGGER.error('Home Assistant core failed to initialize. '
'Further initialization aborted.')
return hass
persistent_notification.setup(hass, config)
yield from persistent_notification.async_setup(hass, config)
_LOGGER.info('Home Assistant core initialized')
_LOGGER.info('Home Assistant core initialized')
# Give event decorators access to HASS
event_decorators.HASS = hass
service.HASS = hass
# Give event decorators access to HASS
event_decorators.HASS = hass
service.HASS = hass
# Setup the components
for domain in loader.load_order_components(components):
_setup_component(hass, domain, config)
# Setup the components
for domain in loader.load_order_components(components):
yield from _async_setup_component(hass, domain, config)
setup_lock.release()
hass.loop.run_until_complete(
hass.loop.run_in_executor(None, component_setup)
)
return hass
@@ -319,30 +443,71 @@ def from_config_file(config_path: str,
if hass is None:
hass = core.HomeAssistant()
@asyncio.coroutine
def _async_init_from_config_file(future):
try:
re_hass = yield from async_from_config_file(
config_path, hass, verbose, skip_pip, log_rotate_days)
future.set_result(re_hass)
# pylint: disable=broad-except
except Exception as exc:
future.set_exception(exc)
# run task
future = asyncio.Future(loop=hass.loop)
hass.loop.create_task(_async_init_from_config_file(future))
hass.loop.run_until_complete(future)
return future.result()
@asyncio.coroutine
def async_from_config_file(config_path: str,
hass: core.HomeAssistant,
verbose: bool=False,
skip_pip: bool=True,
log_rotate_days: Any=None):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
This method is a coroutine.
"""
# Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
yield from hass.loop.run_in_executor(
None, mount_local_lib_path, config_dir)
enable_logging(hass, verbose, log_rotate_days)
try:
config_dict = conf_util.load_yaml_config_file(config_path)
config_dict = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, config_path)
except HomeAssistantError:
return None
finally:
clear_secret_cache()
return from_config_dict(config_dict, hass, enable_log=False,
skip_pip=skip_pip)
hass = yield from async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip)
return hass
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
log_rotate_days=None) -> None:
"""Setup the logging."""
"""Setup the logging.
Async friendly.
"""
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)s")
# suppress overly verbose logs from libraries that aren't helpful
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
@@ -389,36 +554,62 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
'Unable to setup error log %s (access denied)', err_log_path)
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
"""Ensure Home Assistant loader is prepared."""
if not loader.PREPARED:
loader.prepare(hass)
def log_exception(ex, domain, config):
def log_exception(ex, domain, config, hass):
"""Generate log exception for config validation."""
run_callback_threadsafe(
hass.loop, async_log_exception, ex, domain, config, hass).result()
@core.callback
def _async_persistent_notification(hass: core.HomeAssistant, component: str,
link: Optional[bool]=False):
"""Print a persistent notification.
This method must be run in the event loop.
"""
_PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link
_lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name)
if link else name for name, link in _PERSISTENT_ERRORS.items()]
message = ('The following components and platforms could not be set up:\n'
'* ' + '\n* '.join(list(_lst)) + '\nPlease check your config')
persistent_notification.async_create(
hass, message, 'Invalid config', 'invalid_config')
@core.callback
def async_log_exception(ex, domain, config, hass):
"""Generate log exception for config validation.
This method must be run in the event loop.
"""
message = 'Invalid config for [{}]: '.format(domain)
if hass is not None:
_async_persistent_notification(hass, domain, True)
if 'extra keys not allowed' in ex.error_message:
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
.format(ex.path[-1], domain, domain,
'->'.join('%s' % m for m in ex.path))
'->'.join(str(m) for m in ex.path))
else:
message += '{}.'.format(humanize_error(config, ex))
if hasattr(config, '__line__'):
message += " (See {}:{})".format(
config.__config_file__, config.__line__ or '?')
domain_config = config.get(domain, config)
message += " (See {}:{}). ".format(
getattr(domain_config, '__config_file__', '?'),
getattr(domain_config, '__line__', '?'))
if domain != 'homeassistant':
message += (' Please check the docs at '
message += ('Please check the docs at '
'https://home-assistant.io/components/{}/'.format(domain))
_LOGGER.error(message)
def mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path."""
"""Add local library to Python Path.
Async friendly.
"""
deps_dir = os.path.join(config_dir, 'deps')
if deps_dir not in sys.path:
sys.path.insert(0, os.path.join(config_dir, 'deps'))
+22 -10
View File
@@ -7,6 +7,7 @@ Component design guidelines:
format "<DOMAIN>.<OBJECT_ID>".
- Each component should publish services only under its own domain.
"""
import asyncio
import itertools as it
import logging
@@ -79,8 +80,10 @@ def reload_core_config(hass):
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
def setup(hass, config):
@asyncio.coroutine
def async_setup(hass, config):
"""Setup general services related to Home Assistant."""
@asyncio.coroutine
def handle_turn_service(service):
"""Method to handle calls to homeassistant.turn_on/off."""
entity_ids = extract_entity_ids(hass, service)
@@ -96,6 +99,8 @@ def setup(hass, config):
by_domain = it.groupby(sorted(entity_ids),
lambda item: ha.split_entity_id(item)[0])
tasks = []
for domain, ent_ids in by_domain:
# We want to block for all calls and only return when all calls
# have been processed. If a service does not exist it causes a 10
@@ -111,27 +116,34 @@ def setup(hass, config):
# ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids)
hass.services.call(domain, service.service, data, blocking)
tasks.append(hass.services.async_call(
domain, service.service, data, blocking))
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
@asyncio.coroutine
def handle_reload_config(call):
"""Service handler for reloading core config."""
from homeassistant.exceptions import HomeAssistantError
from homeassistant import config as conf_util
try:
path = conf_util.find_config_file(hass.config.config_dir)
conf = conf_util.load_yaml_config_file(path)
conf = yield from conf_util.async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(err)
return
conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
yield from conf_util.async_process_ha_core_config(
hass, conf.get(ha.DOMAIN) or {})
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
handle_reload_config)
hass.services.async_register(
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config)
return True
@@ -39,11 +39,9 @@ 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)
# pylint: disable=too-many-arguments, too-many-instance-attributes
# pylint: disable=abstract-method
class AlarmDotCom(alarm.AlarmControlPanel):
"""Represent an Alarm.com status."""
@@ -56,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."""
@@ -75,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
+132
View File
@@ -0,0 +1,132 @@
"""
Support for Concord232 alarm control panels.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.concord232/
"""
import datetime
import logging
import requests
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.14']
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232'
DEFAULT_PORT = 5007
SCAN_INTERVAL = 1
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Concord232 alarm control panel platform."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
url = 'http://{}:{}'.format(host, port)
try:
add_devices([Concord232Alarm(hass, url, name)])
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
return False
class Concord232Alarm(alarm.AlarmControlPanel):
"""Represents the Concord232-based alarm panel."""
def __init__(self, hass, url, name):
"""Initialize the Concord232 alarm panel."""
from concord232 import client as concord232_client
self._state = STATE_UNKNOWN
self._hass = hass
self._name = name
self._url = url
try:
client = concord232_client.Client(self._url)
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
self._alarm = client
self._alarm.partitions = self._alarm.list_partitions()
self._alarm.last_partition_update = datetime.datetime.now()
self.update()
@property
def should_poll(self):
"""Polling needed."""
return True
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def code_format(self):
"""The characters if code is defined."""
return '[0-9]{4}([0-9]{2})?'
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Update values from API."""
try:
part = self._alarm.list_partitions()[0]
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex))
newstate = STATE_UNKNOWN
except IndexError:
_LOGGER.error("Concord232 reports no partitions")
newstate = STATE_UNKNOWN
if part['arming_level'] == 'Off':
newstate = STATE_ALARM_DISARMED
elif 'Home' in part['arming_level']:
newstate = STATE_ALARM_ARMED_HOME
else:
newstate = STATE_ALARM_ARMED_AWAY
if not newstate == self._state:
_LOGGER.info("State Chnage from %s to %s", self._state, newstate)
self._state = newstate
return self._state
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._alarm.disarm(code)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._alarm.arm('home')
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._alarm.arm('auto')
def alarm_trigger(self, code=None):
"""Alarm trigger command."""
raise NotImplementedError()
@@ -4,26 +4,48 @@ 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
from homeassistant.components.envisalink import (EVL_CONTROLLER,
EnvisalinkDevice,
PARTITION_SCHEMA,
CONF_CODE,
CONF_PANIC,
CONF_PARTITIONNAME,
SIGNAL_PARTITION_UPDATE,
SIGNAL_KEYPAD_UPDATE)
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']
_LOGGER = logging.getLogger(__name__)
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_callback, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Perform the setup for Envisalink alarm panels."""
_configured_partitions = discovery_info['partitions']
_code = discovery_info[CONF_CODE]
@@ -38,30 +60,39 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
_panic_type,
EVL_CONTROLLER.alarm_state['partition'][part_num],
EVL_CONTROLLER)
add_devices_callback([_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
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Represents the Envisalink-based alarm panel."""
"""Representation of an Envisalink-based alarm panel."""
# pylint: disable=too-many-arguments
def __init__(self, partition_number, alarm_name,
code, panic_type, info, controller):
def __init__(self, partition_number, alarm_name, code, panic_type, info,
controller):
"""Initialize the alarm panel."""
from pydispatch import dispatcher
self._partition_number = partition_number
self._code = code
self._panic_type = panic_type
_LOGGER.debug('Setting up alarm: ' + alarm_name)
_LOGGER.debug("Setting up alarm: %s", alarm_name)
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
dispatcher.connect(self._update_callback,
signal=SIGNAL_PARTITION_UPDATE,
sender=dispatcher.Any)
dispatcher.connect(self._update_callback,
signal=SIGNAL_KEYPAD_UPDATE,
sender=dispatcher.Any)
dispatcher.connect(
self._update_callback, signal=SIGNAL_PARTITION_UPDATE,
sender=dispatcher.Any)
dispatcher.connect(
self._update_callback, signal=SIGNAL_KEYPAD_UPDATE,
sender=dispatcher.Any)
def _update_callback(self, partition):
"""Update HA state, if needed."""
@@ -70,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:
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:
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:
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)
@@ -50,8 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes
# pylint: disable=abstract-method
class ManualAlarm(alarm.AlarmControlPanel):
"""
Represents an alarm status.
@@ -131,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):
@@ -145,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):
@@ -157,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):
@@ -55,8 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
config.get(CONF_CODE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes
# pylint: disable=abstract-method
class MqttAlarm(alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status."""
@@ -60,6 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
# talk to the API and trigger a requests exception for setup_platform()
# to catch
self._alarm.list_zones()
self._state = STATE_UNKNOWN
@property
def should_poll(self):
@@ -79,16 +80,20 @@ class NX584Alarm(alarm.AlarmControlPanel):
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Process new events from panel."""
try:
part = self._alarm.list_partitions()[0]
zones = self._alarm.list_zones()
except requests.exceptions.ConnectionError as ex:
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
dict(host=self._url, reason=ex))
return STATE_UNKNOWN
self._state = STATE_UNKNOWN
except IndexError:
_LOGGER.error('nx584 reports no partitions')
return STATE_UNKNOWN
self._state = STATE_UNKNOWN
bypassed = False
for zone in zones:
@@ -100,11 +105,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
break
if not part['armed']:
return STATE_ALARM_DISARMED
self._state = STATE_ALARM_DISARMED
elif bypassed:
return STATE_ALARM_ARMED_HOME
self._state = STATE_ALARM_ARMED_HOME
else:
return STATE_ALARM_ARMED_AWAY
self._state = STATE_ALARM_ARMED_AWAY
def alarm_disarm(self, code=None):
"""Send disarm command."""
@@ -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'
@@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([SimpliSafeAlarm(name, username, password, code)])
# pylint: disable=abstract-method
class SimpliSafeAlarm(alarm.AlarmControlPanel):
"""Representation a SimpliSafe alarm."""
@@ -28,7 +28,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(alarms)
# pylint: disable=abstract-method
class VerisureAlarm(alarm.AlarmControlPanel):
"""Represent a Verisure alarm status."""
+114 -11
View File
@@ -4,19 +4,25 @@ Support for Alexa skill service end point.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/
"""
import asyncio
import copy
import enum
import logging
import uuid
from datetime import datetime
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script, config_validation as cv
from homeassistant.components.http import HomeAssistantView
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
API_ENDPOINT = '/api/alexa'
INTENTS_API_ENDPOINT = '/api/alexa'
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
CONF_ACTION = 'action'
CONF_CARD = 'card'
@@ -28,6 +34,23 @@ CONF_TITLE = 'title'
CONF_CONTENT = 'content'
CONF_TEXT = 'text'
CONF_FLASH_BRIEFINGS = 'flash_briefings'
CONF_UID = 'uid'
CONF_DATE = 'date'
CONF_TITLE = 'title'
CONF_AUDIO = 'audio'
CONF_TEXT = 'text'
CONF_DISPLAY_URL = 'display_url'
ATTR_UID = 'uid'
ATTR_UPDATE_DATE = 'updateDate'
ATTR_TITLE_TEXT = 'titleText'
ATTR_STREAM_URL = 'streamUrl'
ATTR_MAIN_TEXT = 'mainText'
ATTR_REDIRECTION_URL = 'redirectionURL'
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
DOMAIN = 'alexa'
DEPENDENCIES = ['http']
@@ -61,6 +84,16 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_TEXT): cv.template,
}
}
},
CONF_FLASH_BRIEFINGS: {
cv.string: vol.All(cv.ensure_list, [{
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
vol.Optional(CONF_DATE, default=datetime.utcnow()): cv.string,
vol.Required(CONF_TITLE): cv.template,
vol.Optional(CONF_AUDIO): cv.template,
vol.Required(CONF_TEXT, default=""): cv.template,
vol.Optional(CONF_DISPLAY_URL): cv.template,
}]),
}
}
}, extra=vol.ALLOW_EXTRA)
@@ -68,16 +101,19 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config):
"""Activate Alexa component."""
hass.wsgi.register_view(AlexaView(hass,
config[DOMAIN].get(CONF_INTENTS, {})))
intents = config[DOMAIN].get(CONF_INTENTS, {})
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
hass.http.register_view(AlexaIntentsView(hass, intents))
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
return True
class AlexaView(HomeAssistantView):
class AlexaIntentsView(HomeAssistantView):
"""Handle Alexa requests."""
url = API_ENDPOINT
url = INTENTS_API_ENDPOINT
name = 'api:alexa'
def __init__(self, hass, intents):
@@ -94,9 +130,10 @@ class AlexaView(HomeAssistantView):
self.intents = intents
@asyncio.coroutine
def post(self, request):
"""Handle Alexa."""
data = request.json
data = yield from request.json()
_LOGGER.debug('Received Alexa request: %s', data)
@@ -142,7 +179,7 @@ class AlexaView(HomeAssistantView):
action = config.get(CONF_ACTION)
if action is not None:
action.run(response.variables)
yield from action.async_run(response.variables)
# pylint: disable=unsubscriptable-object
if speech is not None:
@@ -184,8 +221,8 @@ class AlexaResponse(object):
self.card = card
return
card["title"] = title.render(self.variables)
card["content"] = content.render(self.variables)
card["title"] = title.async_render(self.variables)
card["content"] = content.async_render(self.variables)
self.card = card
def add_speech(self, speech_type, text):
@@ -195,7 +232,7 @@ class AlexaResponse(object):
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
if isinstance(text, template.Template):
text = text.render(self.variables)
text = text.async_render(self.variables)
self.speech = {
'type': speech_type.value,
@@ -210,7 +247,7 @@ class AlexaResponse(object):
self.reprompt = {
'type': speech_type.value,
key: text.render(self.variables)
key: text.async_render(self.variables)
}
def as_dict(self):
@@ -235,3 +272,69 @@ class AlexaResponse(object):
'sessionAttributes': self.session_attributes,
'response': response,
}
class AlexaFlashBriefingView(HomeAssistantView):
"""Handle Alexa Flash Briefing skill requests."""
url = FLASH_BRIEFINGS_API_ENDPOINT
name = 'api:alexa:flash_briefings'
def __init__(self, hass, flash_briefings):
"""Initialize Alexa view."""
super().__init__(hass)
self.flash_briefings = copy.deepcopy(flash_briefings)
template.attach(hass, self.flash_briefings)
@callback
def get(self, request, briefing_id):
"""Handle Alexa Flash Briefing request."""
_LOGGER.debug('Received Alexa flash briefing request for: %s',
briefing_id)
if self.flash_briefings.get(briefing_id) is None:
err = 'No configured Alexa flash briefing was found for: %s'
_LOGGER.error(err, briefing_id)
return b'', 404
briefing = []
for item in self.flash_briefings.get(briefing_id, []):
output = {}
if item.get(CONF_TITLE) is not None:
if isinstance(item.get(CONF_TITLE), template.Template):
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render()
else:
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
if item.get(CONF_TEXT) is not None:
if isinstance(item.get(CONF_TEXT), template.Template):
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
else:
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
if item.get(CONF_UID) is not None:
output[ATTR_UID] = item.get(CONF_UID)
if item.get(CONF_AUDIO) is not None:
if isinstance(item.get(CONF_AUDIO), template.Template):
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
else:
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
if item.get(CONF_DISPLAY_URL) is not None:
if isinstance(item.get(CONF_DISPLAY_URL),
template.Template):
output[ATTR_REDIRECTION_URL] = \
item[CONF_DISPLAY_URL].async_render()
else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
if isinstance(item[CONF_DATE], str):
item[CONF_DATE] = dt_util.parse_datetime(item[CONF_DATE])
output[ATTR_UPDATE_DATE] = item[CONF_DATE].strftime(DATE_FORMAT)
briefing.append(output)
return self.json(briefing)
+117 -78
View File
@@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/
import asyncio
import json
import logging
import queue
from aiohttp import web
import async_timeout
import homeassistant.core as ha
import homeassistant.remote as rem
@@ -21,7 +23,7 @@ from homeassistant.const import (
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
__version__)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates
from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView
@@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Register the API with the HTTP interface."""
hass.wsgi.register_view(APIStatusView)
hass.wsgi.register_view(APIEventStream)
hass.wsgi.register_view(APIConfigView)
hass.wsgi.register_view(APIDiscoveryView)
hass.wsgi.register_view(APIStatesView)
hass.wsgi.register_view(APIEntityStateView)
hass.wsgi.register_view(APIEventListenersView)
hass.wsgi.register_view(APIEventView)
hass.wsgi.register_view(APIServicesView)
hass.wsgi.register_view(APIDomainServicesView)
hass.wsgi.register_view(APIEventForwardingView)
hass.wsgi.register_view(APIComponentsView)
hass.wsgi.register_view(APIErrorLogView)
hass.wsgi.register_view(APITemplateView)
hass.http.register_view(APIStatusView)
hass.http.register_view(APIEventStream)
hass.http.register_view(APIConfigView)
hass.http.register_view(APIDiscoveryView)
hass.http.register_view(APIStatesView)
hass.http.register_view(APIEntityStateView)
hass.http.register_view(APIEventListenersView)
hass.http.register_view(APIEventView)
hass.http.register_view(APIServicesView)
hass.http.register_view(APIDomainServicesView)
hass.http.register_view(APIEventForwardingView)
hass.http.register_view(APIComponentsView)
hass.http.register_view(APIErrorLogView)
hass.http.register_view(APITemplateView)
return True
@@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView):
url = URL_API
name = "api:status"
@ha.callback
def get(self, request):
"""Retrieve if API is running."""
return self.json_message('API running.')
@@ -71,12 +74,13 @@ class APIEventStream(HomeAssistantView):
url = URL_API_STREAM
name = "api:stream"
@asyncio.coroutine
def get(self, request):
"""Provide a streaming interface for the event bus."""
stop_obj = object()
to_write = queue.Queue()
to_write = asyncio.Queue(loop=self.hass.loop)
restrict = request.args.get('restrict')
restrict = request.GET.get('restrict')
if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
@@ -96,38 +100,40 @@ class APIEventStream(HomeAssistantView):
else:
data = json.dumps(event, cls=rem.JSONEncoder)
to_write.put(data)
yield from to_write.put(data)
def stream():
"""Stream events to response."""
unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events)
response = web.StreamResponse()
response.content_type = 'text/event-stream'
yield from response.prepare(request)
try:
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events)
# Fire off one message so browsers fire open event right away
to_write.put(STREAM_PING_PAYLOAD)
try:
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
while True:
try:
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
# Fire off one message so browsers fire open event right away
yield from to_write.put(STREAM_PING_PAYLOAD)
if payload is stop_obj:
break
while True:
try:
with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=self.hass.loop):
payload = yield from to_write.get()
msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip())
yield msg.encode("UTF-8")
except queue.Empty:
to_write.put(STREAM_PING_PAYLOAD)
except GeneratorExit:
if payload is stop_obj:
break
finally:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
unsub_stream()
return self.Response(stream(), mimetype='text/event-stream')
msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip())
response.write(msg.encode("UTF-8"))
yield from response.drain()
except asyncio.TimeoutError:
yield from to_write.put(STREAM_PING_PAYLOAD)
finally:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
unsub_stream()
class APIConfigView(HomeAssistantView):
@@ -136,6 +142,7 @@ class APIConfigView(HomeAssistantView):
url = URL_API_CONFIG
name = "api:config"
@ha.callback
def get(self, request):
"""Get current configuration."""
return self.json(self.hass.config.as_dict())
@@ -148,6 +155,7 @@ class APIDiscoveryView(HomeAssistantView):
url = URL_API_DISCOVERY_INFO
name = "api:discovery"
@ha.callback
def get(self, request):
"""Get discovery info."""
needs_auth = self.hass.config.api.api_password is not None
@@ -165,17 +173,19 @@ class APIStatesView(HomeAssistantView):
url = URL_API_STATES
name = "api:states"
@ha.callback
def get(self, request):
"""Get current states."""
return self.json(self.hass.states.all())
return self.json(self.hass.states.async_all())
class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests."""
url = "/api/states/<entity(exist=False):entity_id>"
url = "/api/states/{entity_id}"
name = "api:entity-state"
@ha.callback
def get(self, request, entity_id):
"""Retrieve state of entity."""
state = self.hass.states.get(entity_id)
@@ -184,34 +194,41 @@ class APIEntityStateView(HomeAssistantView):
else:
return self.json_message('Entity not found', HTTP_NOT_FOUND)
@asyncio.coroutine
def post(self, request, entity_id):
"""Update state of entity."""
try:
new_state = request.json['state']
except KeyError:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON specified',
HTTP_BAD_REQUEST)
new_state = data.get('state')
if not new_state:
return self.json_message('No state specified', HTTP_BAD_REQUEST)
attributes = request.json.get('attributes')
force_update = request.json.get('force_update', False)
attributes = data.get('attributes')
force_update = data.get('force_update', False)
is_new_state = self.hass.states.get(entity_id) is None
# Write state
self.hass.states.set(entity_id, new_state, attributes, force_update)
self.hass.states.async_set(entity_id, new_state, attributes,
force_update)
# Read the state back for our response
resp = self.json(self.hass.states.get(entity_id))
if is_new_state:
resp.status_code = HTTP_CREATED
status_code = HTTP_CREATED if is_new_state else 200
resp = self.json(self.hass.states.get(entity_id), status_code)
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
return resp
@ha.callback
def delete(self, request, entity_id):
"""Remove entity."""
if self.hass.states.remove(entity_id):
if self.hass.states.async_remove(entity_id):
return self.json_message('Entity removed')
else:
return self.json_message('Entity not found', HTTP_NOT_FOUND)
@@ -223,20 +240,23 @@ class APIEventListenersView(HomeAssistantView):
url = URL_API_EVENTS
name = "api:event-listeners"
@ha.callback
def get(self, request):
"""Get event listeners."""
return self.json(events_json(self.hass))
return self.json(async_events_json(self.hass))
class APIEventView(HomeAssistantView):
"""View to handle Event requests."""
url = '/api/events/<event_type>'
url = '/api/events/{event_type}'
name = "api:event"
@asyncio.coroutine
def post(self, request, event_type):
"""Fire events."""
event_data = request.json
body = yield from request.text()
event_data = json.loads(body) if body else None
if event_data is not None and not isinstance(event_data, dict):
return self.json_message('Event data should be a JSON object',
@@ -251,7 +271,7 @@ class APIEventView(HomeAssistantView):
if state:
event_data[key] = state
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote)
return self.json_message("Event {} fired.".format(event_type))
@@ -262,24 +282,30 @@ class APIServicesView(HomeAssistantView):
url = URL_API_SERVICES
name = "api:services"
@ha.callback
def get(self, request):
"""Get registered services."""
return self.json(services_json(self.hass))
return self.json(async_services_json(self.hass))
class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests."""
url = "/api/services/<domain>/<service>"
url = "/api/services/{domain}/{service}"
name = "api:domain-services"
@asyncio.coroutine
def post(self, request, domain, service):
"""Call a service.
Returns a list of changed states.
"""
with TrackStates(self.hass) as changed_states:
self.hass.services.call(domain, service, request.json, True)
body = yield from request.text()
data = json.loads(body) if body else None
with AsyncTrackStates(self.hass) as changed_states:
yield from self.hass.services.async_call(domain, service, data,
True)
return self.json(changed_states)
@@ -291,11 +317,14 @@ class APIEventForwardingView(HomeAssistantView):
name = "api:event-forward"
event_forwarder = None
@asyncio.coroutine
def post(self, request):
"""Setup an event forwarder."""
data = request.json
if data is None:
try:
data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
host = data['host']
api_password = data['api_password']
@@ -311,21 +340,25 @@ class APIEventForwardingView(HomeAssistantView):
api = rem.API(host, api_password, port)
if not api.validate_api():
valid = yield from self.hass.loop.run_in_executor(
None, api.validate_api)
if not valid:
return self.json_message("Unable to validate API.",
HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is None:
self.event_forwarder = rem.EventForwarder(self.hass)
self.event_forwarder.connect(api)
self.event_forwarder.async_connect(api)
return self.json_message("Event forwarding setup.")
@asyncio.coroutine
def delete(self, request):
"""Remove event forwarer."""
data = request.json
if data is None:
"""Remove event forwarder."""
try:
data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
@@ -342,7 +375,7 @@ class APIEventForwardingView(HomeAssistantView):
if self.event_forwarder is not None:
api = rem.API(host, None, port)
self.event_forwarder.disconnect(api)
self.event_forwarder.async_disconnect(api)
return self.json_message("Event forwarding cancelled.")
@@ -353,6 +386,7 @@ class APIComponentsView(HomeAssistantView):
url = URL_API_COMPONENTS
name = "api:components"
@ha.callback
def get(self, request):
"""Get current loaded components."""
return self.json(self.hass.config.components)
@@ -364,9 +398,12 @@ class APIErrorLogView(HomeAssistantView):
url = URL_API_ERROR_LOG
name = "api:error-log"
@asyncio.coroutine
def get(self, request):
"""Serve error log."""
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
resp = yield from self.file(
request, self.hass.config.path(ERROR_LOG_FILENAME))
return resp
class APITemplateView(HomeAssistantView):
@@ -375,23 +412,25 @@ class APITemplateView(HomeAssistantView):
url = URL_API_TEMPLATE
name = "api:template"
@asyncio.coroutine
def post(self, request):
"""Render a template."""
try:
tpl = template.Template(request.json['template'], self.hass)
return tpl.render(request.json.get('variables'))
except TemplateError as ex:
data = yield from request.json()
tpl = template.Template(data['template'], self.hass)
return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex:
return self.json_message('Error rendering template: {}'.format(ex),
HTTP_BAD_REQUEST)
def services_json(hass):
def async_services_json(hass):
"""Generate services data to JSONify."""
return [{"domain": key, "services": value}
for key, value in hass.services.services.items()]
for key, value in hass.services.async_services().items()]
def events_json(hass):
def async_events_json(hass):
"""Generate event data to JSONify."""
return [{"event": key, "listener_count": value}
for key, value in hass.bus.listeners.items()]
for key, value in hass.bus.async_listeners().items()]
+55 -49
View File
@@ -11,7 +11,7 @@ import os
import voluptuous as vol
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.bootstrap import async_prepare_setup_platform
from homeassistant import config as conf_util
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
@@ -24,7 +24,6 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'automation'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@@ -67,6 +66,7 @@ def _platform_validator(config):
return getattr(platform, 'TRIGGER_SCHEMA')(config)
_TRIGGER_SCHEMA = vol.All(
cv.ensure_list,
[
@@ -143,68 +143,75 @@ def reload(hass):
hass.services.call(DOMAIN, SERVICE_RELOAD)
def setup(hass, config):
@asyncio.coroutine
def async_setup(hass, config):
"""Setup the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_AUTOMATIONS)
success = run_coroutine_threadsafe(
_async_process_config(hass, config, component), hass.loop).result()
success = yield from _async_process_config(hass, config, component)
if not success:
return False
descriptions = conf_util.load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
descriptions = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
@asyncio.coroutine
def trigger_service_handler(service_call):
"""Handle automation triggers."""
for entity in component.extract_from_service(service_call):
hass.loop.create_task(entity.async_trigger(
tasks = []
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.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls."""
tasks = []
method = 'async_{}'.format(service_call.service)
for entity in component.extract_from_service(service_call):
hass.loop.create_task(getattr(entity, method)())
for entity in component.async_extract_from_service(service_call):
tasks.append(getattr(entity, method)())
yield from asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def toggle_service_handler(service_call):
"""Handle automation toggle service calls."""
for entity in component.extract_from_service(service_call):
tasks = []
for entity in component.async_extract_from_service(service_call):
if entity.is_on:
hass.loop.create_task(entity.async_turn_off())
tasks.append(entity.async_turn_off())
else:
hass.loop.create_task(entity.async_turn_on())
tasks.append(entity.async_turn_on())
yield from asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def reload_service_handler(service_call):
"""Remove all automations and load new ones from config."""
conf = yield from hass.loop.run_in_executor(
None, component.prepare_reload)
conf = yield from component.async_prepare_reload()
if conf is None:
return
hass.loop.create_task(_async_process_config(hass, conf, component))
yield from _async_process_config(hass, conf, component)
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
descriptions.get(SERVICE_TRIGGER),
schema=TRIGGER_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler,
descriptions.get(SERVICE_RELOAD),
schema=RELOAD_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler,
descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
descriptions.get(SERVICE_TOGGLE),
schema=SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA)
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
hass.services.register(DOMAIN, service, turn_onoff_service_handler,
descriptions.get(service),
schema=SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, service, turn_onoff_service_handler,
descriptions.get(service), schema=SERVICE_SCHEMA)
return True
@@ -212,8 +219,6 @@ def setup(hass, config):
class AutomationEntity(ToggleEntity):
"""Entity to show status of entity."""
# pylint: disable=abstract-method
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, name, async_attach_triggers, cond_func, async_action,
hidden):
"""Initialize an automation entity."""
@@ -260,7 +265,7 @@ class AutomationEntity(ToggleEntity):
return
yield from self.async_enable()
self.hass.loop.create_task(self.async_update_ha_state())
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
@@ -271,7 +276,7 @@ class AutomationEntity(ToggleEntity):
self._async_detach_triggers()
self._async_detach_triggers = None
self._enabled = False
self.hass.loop.create_task(self.async_update_ha_state())
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_trigger(self, variables, skip_condition=False):
@@ -280,15 +285,15 @@ class AutomationEntity(ToggleEntity):
This method is a coroutine.
"""
if skip_condition or self._cond_func(variables):
yield from self._async_action(variables)
yield from self._async_action(self.entity_id, variables)
self._last_triggered = utcnow()
self.hass.loop.create_task(self.async_update_ha_state())
yield from self.async_update_ha_state()
def remove(self):
@asyncio.coroutine
def async_remove(self):
"""Remove automation from HASS."""
run_coroutine_threadsafe(self.async_turn_off(),
self.hass.loop).result()
super().remove()
yield from self.async_turn_off()
yield from super().async_remove()
@asyncio.coroutine
def async_enable(self):
@@ -341,12 +346,13 @@ def _async_process_config(hass, config, component):
entity = AutomationEntity(name, async_attach_triggers, cond_func,
action, hidden)
if config_block[CONF_INITIAL_STATE]:
tasks.append(hass.loop.create_task(entity.async_enable()))
tasks.append(entity.async_enable())
entities.append(entity)
yield from asyncio.gather(*tasks, loop=hass.loop)
yield from hass.loop.run_in_executor(
None, component.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
@@ -356,11 +362,12 @@ def _async_get_action(hass, config, name):
script_obj = script.Script(hass, config, name)
@asyncio.coroutine
def action(variables=None):
def action(entity_id, variables):
"""Action to be executed."""
_LOGGER.info('Executing %s', name)
logbook.async_log_entry(hass, name, 'has been triggered', DOMAIN)
hass.loop.create_task(script_obj.async_run(variables))
logbook.async_log_entry(
hass, name, 'has been triggered', DOMAIN, entity_id)
yield from script_obj.async_run(variables)
return action
@@ -393,9 +400,8 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
removes = []
for conf in trigger_configs:
platform = yield from hass.loop.run_in_executor(
None, prepare_setup_platform, hass, config, DOMAIN,
conf.get(CONF_PLATFORM))
platform = yield from async_prepare_setup_platform(
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
if platform is None:
return None
@@ -0,0 +1,41 @@
"""
Trigger an automation when a LiteJet switch is released.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/automation.litejet/
"""
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['litejet']
_LOGGER = logging.getLogger(__name__)
CONF_NUMBER = 'number'
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'litejet',
vol.Required(CONF_NUMBER): cv.positive_int
})
def async_trigger(hass, config, action):
"""Listen for events based on configuration."""
number = config.get(CONF_NUMBER)
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, {
'trigger': {
CONF_PLATFORM: 'litejet',
CONF_NUMBER: number
},
})
hass.data['litejet_system'].on_switch_released(number, call_action)
@@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensor_class, pin)])
# pylint: disable=too-many-instance-attributes, too-many-arguments
class ArestBinarySensor(BinarySensorDevice):
"""Implement an aREST binary sensor for a pin."""
@@ -93,7 +92,6 @@ class ArestBinarySensor(BinarySensorDevice):
self.arest.update()
# pylint: disable=too-few-public-methods
class ArestData(object):
"""Class for handling the data retrieval for pins."""
@@ -53,7 +53,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
value_template)])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class CommandBinarySensor(BinarySensorDevice):
"""Represent a command line binary sensor."""
+136
View File
@@ -0,0 +1,136 @@
"""
Support for exposing Concord232 elements as sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.concord232/
"""
import datetime
import logging
import requests
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.14']
_LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE_ZONES = 'exclude_zones'
CONF_ZONE_TYPES = 'zone_types'
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Alarm'
DEFAULT_PORT = '5007'
DEFAULT_SSL = False
SCAN_INTERVAL = 1
ZONE_TYPES_SCHEMA = vol.Schema({
cv.positive_int: vol.In(SENSOR_CLASSES),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
vol.All(cv.ensure_list, [cv.positive_int]),
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Concord232 binary sensor platform."""
from concord232 import client as concord232_client
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
exclude = config.get(CONF_EXCLUDE_ZONES)
zone_types = config.get(CONF_ZONE_TYPES)
sensors = []
try:
_LOGGER.debug("Initializing Client")
client = concord232_client.Client('http://{}:{}'.format(host, port))
client.zones = client.list_zones()
client.last_zone_update = datetime.datetime.now()
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
return False
for zone in client.zones:
_LOGGER.info("Loading Zone found: %s", zone['name'])
if zone['number'] not in exclude:
sensors.append(
Concord232ZoneSensor(
hass, client, zone, zone_types.get(zone['number'],
get_opening_type(zone)))
)
add_devices(sensors)
return True
def get_opening_type(zone):
"""Helper function to try to guess sensor type from name."""
if 'MOTION' in zone['name']:
return 'motion'
if 'KEY' in zone['name']:
return 'safety'
if 'SMOKE' in zone['name']:
return 'smoke'
if 'WATER' in zone['name']:
return 'water'
return 'opening'
class Concord232ZoneSensor(BinarySensorDevice):
"""Representation of a Concord232 zone as a sensor."""
def __init__(self, hass, client, zone, zone_type):
"""Initialize the Concord232 binary sensor."""
self._hass = hass
self._client = client
self._zone = zone
self._number = zone['number']
self._zone_type = zone_type
self.update()
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return self._zone_type
@property
def should_poll(self):
"""No polling needed."""
return True
@property
def name(self):
"""Return the name of the binary sensor."""
return self._zone['name']
@property
def is_on(self):
"""Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state"
return bool(self._zone['state'] == 'Normal')
def update(self):
""""Get updated stats from API."""
last_update = datetime.datetime.now() - self._client.last_zone_update
_LOGGER.debug("Zone: %s ", self._zone)
if last_update > datetime.timedelta(seconds=1):
self._client.zones = self._client.list_zones()
self._client.last_zone_update = datetime.datetime.now()
_LOGGER.debug("Updated from Zone: %s", self._zone['name'])
if hasattr(self._client, 'zones'):
self._zone = next((x for x in self._client.zones
if x['number'] == self._number), None)
@@ -35,7 +35,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
"""Representation of an Envisalink binary sensor."""
# pylint: disable=too-many-arguments
def __init__(self, zone_number, zone_name, zone_type, info, controller):
"""Initialize the binary_sensor."""
from pydispatch import dispatcher
@@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
from haffmpeg import SensorNoise, SensorMotion
# check source
if not run_test(config.get(CONF_INPUT)):
if not run_test(hass, config.get(CONF_INPUT)):
return
# generate sensor object
@@ -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."""
@@ -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)
@@ -51,7 +52,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttBinarySensor(BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT."""
@@ -67,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)
@@ -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
+14 -4
View File
@@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.sensor.nest import NestSensor
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
import homeassistant.components.nest as nest
from homeassistant.components.nest import DATA_NEST
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['nest']
@@ -35,9 +35,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Nest binary sensors."""
nest = hass.data[DATA_NEST]
all_sensors = []
for structure, device in nest.devices():
add_devices([NestBinarySensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]])
all_sensors.extend(
[NestBinarySensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]])
add_devices(all_sensors, True)
class NestBinarySensor(NestSensor, BinarySensorDevice):
@@ -46,4 +52,8 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
@property
def is_on(self):
"""True if the binary sensor is on."""
return bool(getattr(self.device, self.variable))
return self._state
def update(self):
"""Retrieve latest state."""
self._state = bool(getattr(self.device, self.variable))
@@ -0,0 +1,133 @@
"""
Support for the Netatmo binary sensors.
The binary sensors based on events seen by the NetatmoCamera
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.netatmo/
"""
import logging
import voluptuous as vol
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, CONF_TIMEOUT
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ["netatmo"]
_LOGGER = logging.getLogger(__name__)
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
"Someone known": "motion",
"Someone unknown": "motion",
"Motion": "motion",
}
CONF_HOME = 'home'
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()):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
# pylint: disable=unused-argument
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:
data = WelcomeData(netatmo.NETATMO_AUTH, home)
if data.get_camera_names() == []:
return None
except lnetatmo.NoDevice:
return None
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)
for camera_name in data.get_camera_names():
if CONF_CAMERAS in config:
if config[CONF_CAMERAS] != [] and \
camera_name not in config[CONF_CAMERAS]:
continue
for variable in sensors:
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, 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:
self._name = camera_name
self._sensor_name = sensor
self._name += ' ' + sensor
camera_id = data.welcomedata.cameraByName(camera=camera_name,
home=home)['id']
self._unique_id = "Welcome_binary_sensor {0} - {1}".format(self._name,
camera_id)
self.update()
@property
def name(self):
"""The name of the Netatmo device and this sensor."""
return self._name
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self._unique_id
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return SENSOR_TYPES.get(self._sensor_name)
@property
def is_on(self):
"""Return true if binary sensor is on."""
return self._state
def update(self):
"""Request an update from the Netatmo API."""
self._data.update()
self._data.welcomedata.updateEvent(home=self._data.home)
if self._sensor_name == "Someone known":
self._state =\
self._data.welcomedata.someoneKnownSeen(self._home,
self._camera_name,
self._timeout*60)
elif self._sensor_name == "Someone unknown":
self._state =\
self._data.welcomedata.someoneUnknownSeen(self._home,
self._camera_name,
self._timeout*60)
elif self._sensor_name == "Motion":
self._state =\
self._data.welcomedata.motionDetected(self._home,
self._camera_name,
self._timeout*60)
else:
return None
@@ -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:
@@ -58,11 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices)
# pylint: disable=too-many-instance-attributes
class OctoPrintBinarySensor(BinarySensorDevice):
"""Representation an OctoPrint binary sensor."""
# pylint: disable=too-many-arguments
def __init__(self, api, condition, sensor_type, sensor_name, unit,
endpoint, group, tool=None):
"""Initialize a new OctoPrint sensor."""
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rest/
"""
import logging
import json
import voluptuous as vol
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
@@ -30,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_HEADERS): cv.string,
vol.Optional(CONF_HEADERS): {cv.string: cv.string},
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
@@ -42,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
# pylint: disable=unused-variable, too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the REST binary sensor."""
name = config.get(CONF_NAME)
@@ -52,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
verify_ssl = config.get(CONF_VERIFY_SSL)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
headers = json.loads(config.get(CONF_HEADERS, '{}'))
headers = config.get(CONF_HEADERS)
sensor_class = config.get(CONF_SENSOR_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
@@ -70,14 +68,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rest.update()
if rest.data is None:
_LOGGER.error('Unable to fetch REST data')
_LOGGER.error("Unable to fetch REST data from %s", resource)
return False
add_devices([RestBinarySensor(
hass, rest, name, sensor_class, value_template)])
# pylint: disable=too-many-arguments
class RestBinarySensor(BinarySensorDevice):
"""Representation of a REST binary sensor."""
@@ -109,8 +106,8 @@ class RestBinarySensor(BinarySensorDevice):
return False
if self._value_template is not None:
response = self._value_template.render_with_possible_json_value(
self.rest.data, False)
response = self._value_template.\
async_render_with_possible_json_value(self.rest.data, False)
try:
return bool(int(response))
@@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(binary_sensors)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class RPiGPIOBinarySensor(BinarySensorDevice):
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
@@ -73,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)
@@ -25,7 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(dev)
# pylint: disable=too-many-instance-attributes
class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
"""Implementation of a SleepIQ presence sensor."""
+9 -10
View File
@@ -7,21 +7,20 @@ https://home-assistant.io/components/binary_sensor.tcp/
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.sensor.tcp import Sensor, CONF_VALUE_ON
from homeassistant.components.sensor.tcp import (
TcpSensor, CONF_VALUE_ON, PLATFORM_SCHEMA)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the binary sensor."""
if not BinarySensor.validate_config(config):
return False
add_entities((BinarySensor(hass, config),))
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
class BinarySensor(BinarySensorDevice, Sensor):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the TCP binary sensor."""
add_devices([TcpBinarySensor(hass, config)])
class TcpBinarySensor(BinarySensorDevice, TcpSensor):
"""A binary sensor which is on when its state == CONF_VALUE_ON."""
required = (CONF_VALUE_ON,)
@@ -17,8 +17,8 @@ from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
CONF_SENSOR_CLASS, CONF_SENSORS)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -35,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup template binary sensors."""
sensors = []
@@ -61,34 +62,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if not sensors:
_LOGGER.error('No sensors added')
return False
add_devices(sensors)
yield from async_add_devices(sensors, True)
return True
class BinarySensorTemplate(BinarySensorDevice):
"""A virtual binary sensor that triggers from another sensor."""
# pylint: disable=too-many-arguments
def __init__(self, hass, device, friendly_name, sensor_class,
value_template, entity_ids):
"""Initialize the Template binary sensor."""
self.hass = hass
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
hass=hass)
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device,
hass=hass)
self._name = friendly_name
self._sensor_class = sensor_class
self._template = value_template
self._state = None
self.update()
@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)
track_state_change(hass, entity_ids, template_bsensor_state_listener)
async_track_state_change(
hass, entity_ids, template_bsensor_state_listener)
@property
def name(self):
@@ -112,7 +111,7 @@ class BinarySensorTemplate(BinarySensorDevice):
@asyncio.coroutine
def async_update(self):
"""Get the latest data and update the state."""
"""Update the state from the template."""
try:
self._state = self._template.async_render().lower() == 'true'
except TemplateError as ex:
@@ -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 (
@@ -72,7 +75,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class SensorTrend(BinarySensorDevice):
"""Representation of a trend Sensor."""
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, hass, device_id, friendly_name,
target_entity, attribute, sensor_class, invert):
"""Initialize the sensor."""
@@ -88,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)
@@ -119,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
@@ -16,9 +16,9 @@ DEPENDENCIES = ['vera']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Perform the setup for Vera controller devices."""
add_devices_callback(
add_devices(
VeraBinarySensor(device, VERA_CONTROLLER)
for device in VERA_DEVICES['binary_sensor'])
@@ -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):
+12 -6
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):
@@ -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
+1 -1
View File
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument,too-few-public-methods
# pylint: disable=unused-argument
def setup(hass, config):
"""Setup BloomSky component."""
api_key = config[DOMAIN][CONF_API_KEY]
@@ -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']
+82
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)
@@ -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
+75 -44
View File
@@ -5,8 +5,10 @@ Component to interface with cameras.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/
"""
import asyncio
import logging
import time
from aiohttp import web
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@@ -25,17 +27,16 @@ STATE_IDLE = 'idle'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
# pylint: disable=too-many-branches
def setup(hass, config):
@asyncio.coroutine
def async_setup(hass, config):
"""Setup the camera component."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.wsgi.register_view(CameraImageView(hass, component.entities))
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
component.setup(config)
hass.http.register_view(CameraImageView(hass, component.entities))
hass.http.register_view(CameraMjpegStream(hass, component.entities))
yield from component.async_setup(config)
return True
@@ -80,33 +81,58 @@ class Camera(Entity):
"""Return bytes of camera image."""
raise NotImplementedError()
def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from camera images."""
def stream():
"""Stream images as mjpeg stream."""
try:
last_image = None
while True:
img_bytes = self.camera_image()
@asyncio.coroutine
def async_camera_image(self):
"""Return bytes of camera image.
if img_bytes is not None and img_bytes != last_image:
yield bytes(
'--jpegboundary\r\n'
'Content-Type: image/jpeg\r\n'
'Content-Length: {}\r\n\r\n'.format(
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
This method must be run in the event loop.
"""
image = yield from self.hass.loop.run_in_executor(
None, self.camera_image)
return image
last_image = img_bytes
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images.
time.sleep(0.5)
except GeneratorExit:
pass
This method must be run in the event loop.
"""
response = web.StreamResponse()
return response(
stream(),
content_type=('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
)
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
yield from response.prepare(request)
def write(img_bytes):
"""Write image to stream."""
response.write(bytes(
'--jpegboundary\r\n'
'Content-Type: image/jpeg\r\n'
'Content-Length: {}\r\n\r\n'.format(
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
last_image = None
try:
while True:
img_bytes = yield from self.async_camera_image()
if not img_bytes:
break
if img_bytes is not None and img_bytes != last_image:
write(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
write(img_bytes)
last_image = img_bytes
yield from response.drain()
yield from asyncio.sleep(.5)
finally:
yield from response.write_eof()
@property
def state(self):
@@ -144,22 +170,25 @@ class CameraView(HomeAssistantView):
super().__init__(hass)
self.entities = entities
@asyncio.coroutine
def get(self, request, entity_id):
"""Start a get request."""
camera = self.entities.get(entity_id)
if camera is None:
return self.Response(status=404)
return web.Response(status=404)
authenticated = (request.authenticated or
request.args.get('token') == camera.access_token)
request.GET.get('token') == camera.access_token)
if not authenticated:
return self.Response(status=401)
return web.Response(status=401)
return self.handle(camera)
response = yield from self.handle(request, camera)
return response
def handle(self, camera):
@asyncio.coroutine
def handle(self, request, camera):
"""Hanlde the camera request."""
raise NotImplementedError()
@@ -167,25 +196,27 @@ class CameraView(HomeAssistantView):
class CameraImageView(CameraView):
"""Camera view to serve an image."""
url = "/api/camera_proxy/<entity(domain=camera):entity_id>"
url = "/api/camera_proxy/{entity_id}"
name = "api:camera:image"
def handle(self, camera):
@asyncio.coroutine
def handle(self, request, camera):
"""Serve camera image."""
response = camera.camera_image()
image = yield from camera.async_camera_image()
if response is None:
return self.Response(status=500)
if image is None:
return web.Response(status=500)
return self.Response(response)
return web.Response(body=image)
class CameraMjpegStream(CameraView):
"""Camera View to serve an MJPEG stream."""
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>"
url = "/api/camera_proxy_stream/{entity_id}"
name = "api:camera:stream"
def handle(self, camera):
@asyncio.coroutine
def handle(self, request, camera):
"""Serve camera image."""
return camera.mjpeg_stream(self.Response)
yield from camera.handle_async_mjpeg_stream(request)
+43 -19
View File
@@ -4,15 +4,18 @@ Support for Cameras with FFmpeg as decoder.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.ffmpeg/
"""
import asyncio
import logging
import voluptuous as vol
from aiohttp import web
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import (
run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
from homeassistant.util.async import run_coroutine_threadsafe
DEPENDENCIES = ['ffmpeg']
@@ -27,17 +30,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def setup_platform(hass, config, add_devices, discovery_info=None):
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a FFmpeg Camera."""
if not run_test(config.get(CONF_INPUT)):
if not async_run_test(hass, config.get(CONF_INPUT)):
return
add_devices([FFmpegCamera(config)])
yield from async_add_devices([FFmpegCamera(hass, config)])
class FFmpegCamera(Camera):
"""An implementation of an FFmpeg camera."""
def __init__(self, config):
def __init__(self, hass, config):
"""Initialize a FFmpeg camera."""
super().__init__()
self._name = config.get(CONF_NAME)
@@ -45,24 +49,44 @@ class FFmpegCamera(Camera):
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
from haffmpeg import ImageSingle, IMAGE_JPEG
ffmpeg = ImageSingle(get_binary())
from haffmpeg import ImageSingleAsync, IMAGE_JPEG
ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop)
return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments)
image = yield from ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments)
return image
def mjpeg_stream(self, response):
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
from haffmpeg import CameraMjpegAsync
stream = CameraMjpeg(get_binary())
stream.open_camera(self._input, extra_cmd=self._extra_arguments)
return response(
stream,
mimetype='multipart/x-mixed-replace;boundary=ffserver',
direct_passthrough=True
)
stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop)
yield from stream.open_camera(
self._input, extra_cmd=self._extra_arguments)
response = web.StreamResponse()
response.content_type = 'multipart/x-mixed-replace;boundary=ffserver'
yield from response.prepare(request)
try:
while True:
data = yield from stream.read(102400)
if not data:
break
response.write(data)
finally:
self.hass.async_add_job(stream.close())
yield from response.write_eof()
@property
def name(self):
@@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([FoscamCamera(config)])
# pylint: disable=too-many-instance-attributes
class FoscamCamera(Camera):
"""An implementation of a Foscam IP camera."""
+46 -14
View File
@@ -4,10 +4,13 @@ Support for IP Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.generic/
"""
import asyncio
import logging
import aiohttp
import async_timeout
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from requests.auth import HTTPDigestAuth
import voluptuous as vol
from homeassistant.const import (
@@ -16,6 +19,7 @@ from homeassistant.const import (
from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
@@ -35,13 +39,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a generic IP Camera."""
add_devices([GenericCamera(hass, config)])
yield from async_add_devices([GenericCamera(hass, config)])
# pylint: disable=too-many-instance-attributes
class GenericCamera(Camera):
"""A generic implementation of an IP camera."""
@@ -49,6 +53,7 @@ class GenericCamera(Camera):
"""Initialize a generic camera."""
super().__init__()
self.hass = hass
self._authentication = device_info.get(CONF_AUTHENTICATION)
self._name = device_info.get(CONF_NAME)
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass
@@ -58,10 +63,10 @@ class GenericCamera(Camera):
password = device_info.get(CONF_PASSWORD)
if username and password:
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION:
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
self._auth = HTTPDigestAuth(username, password)
else:
self._auth = HTTPBasicAuth(username, password)
self._auth = aiohttp.BasicAuth(username, password=password)
else:
self._auth = None
@@ -69,9 +74,15 @@ class GenericCamera(Camera):
self._last_image = None
def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
try:
url = self._still_image_url.render()
url = self._still_image_url.async_render()
except TemplateError as err:
_LOGGER.error('Error parsing template %s: %s',
self._still_image_url, err)
@@ -80,16 +91,37 @@ class GenericCamera(Camera):
if url == self._last_url and self._limit_refetch:
return self._last_image
kwargs = {'timeout': 10, 'auth': self._auth}
# aiohttp don't support DigestAuth yet
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
def fetch():
"""Read image from a URL."""
try:
kwargs = {'timeout': 10, 'auth': self._auth}
response = requests.get(url, **kwargs)
return response.content
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return self._last_image
try:
response = requests.get(url, **kwargs)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
self._last_image = yield from self.hass.loop.run_in_executor(
None, fetch)
# async
else:
try:
with async_timeout.timeout(10, loop=self.hass.loop):
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
self._last_image = response.content
return self._last_image
@property
+54 -21
View File
@@ -4,9 +4,14 @@ Support for IP Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.mjpeg/
"""
import asyncio
import logging
from contextlib import closing
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
@@ -34,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a MJPEG IP Camera."""
add_devices([MjpegCamera(config)])
yield from async_add_devices([MjpegCamera(hass, config)])
def extract_image_from_mjpeg(stream):
@@ -52,11 +58,10 @@ def extract_image_from_mjpeg(stream):
return jpg
# pylint: disable=too-many-instance-attributes
class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL."""
def __init__(self, device_info):
def __init__(self, hass, device_info):
"""Initialize a MJPEG camera."""
super().__init__()
self._name = device_info.get(CONF_NAME)
@@ -65,32 +70,60 @@ class MjpegCamera(Camera):
self._password = device_info.get(CONF_PASSWORD)
self._mjpeg_url = device_info[CONF_MJPEG_URL]
def camera_stream(self):
"""Return a MJPEG stream image response directly from the camera."""
self._auth = None
if self._username and self._password:
if self._authentication == HTTP_BASIC_AUTHENTICATION:
self._auth = aiohttp.BasicAuth(
self._username, password=self._password
)
def camera_image(self):
"""Return a still image response from the camera."""
if self._username and self._password:
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(self._username, self._password)
else:
auth = HTTPBasicAuth(self._username, self._password)
return requests.get(self._mjpeg_url,
auth=auth,
stream=True, timeout=10)
req = requests.get(
self._mjpeg_url, auth=auth, stream=True, timeout=10)
else:
return requests.get(self._mjpeg_url, stream=True, timeout=10)
req = requests.get(self._mjpeg_url, stream=True, timeout=10)
def camera_image(self):
"""Return a still image response from the camera."""
with closing(self.camera_stream()) as response:
return extract_image_from_mjpeg(response.iter_content(1024))
with closing(req) as response:
return extract_image_from_mjpeg(response.iter_content(102400))
def mjpeg_stream(self, response):
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
stream = self.camera_stream()
return response(
stream.iter_content(chunk_size=1024),
mimetype=stream.headers[CONTENT_TYPE_HEADER],
direct_passthrough=True
)
# aiohttp don't support DigestAuth -> Fallback
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
yield from super().handle_async_mjpeg_stream(request)
return
# connect to stream
try:
with async_timeout.timeout(10, loop=self.hass.loop):
stream = yield from self.hass.websession.get(
self._mjpeg_url,
auth=self._auth
)
except asyncio.TimeoutError:
raise HTTPGatewayTimeout()
response = web.StreamResponse()
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
yield from response.prepare(request)
try:
while True:
data = yield from stream.content.read(102400)
if not data:
break
response.write(data)
finally:
self.hass.async_add_job(stream.release())
yield from response.write_eof()
@property
def name(self):
+15 -38
View File
@@ -5,12 +5,11 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.netatmo/
"""
import logging
from datetime import timedelta
import requests
import voluptuous as vol
from homeassistant.util import Throttle
from homeassistant.components.netatmo import WelcomeData
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.loader import get_component
from homeassistant.helpers import config_validation as cv
@@ -22,8 +21,6 @@ _LOGGER = logging.getLogger(__name__)
CONF_HOME = 'home'
CONF_CAMERAS = 'cameras'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME): cv.string,
vol.Optional(CONF_CAMERAS, default=[]):
@@ -39,15 +36,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
import lnetatmo
try:
data = WelcomeData(netatmo.NETATMO_AUTH, home)
for camera_name in data.get_camera_names():
if CONF_CAMERAS in config:
if config[CONF_CAMERAS] != [] and \
camera_name not in config[CONF_CAMERAS]:
continue
add_devices([WelcomeCamera(data, camera_name, home)])
except lnetatmo.NoDevice:
return None
for camera_name in data.get_camera_names():
if config[CONF_CAMERAS] != []:
if camera_name not in config[CONF_CAMERAS]:
continue
add_devices([WelcomeCamera(data, camera_name, home)])
class WelcomeCamera(Camera):
"""Representation of the images published from Welcome camera."""
@@ -61,6 +58,10 @@ class WelcomeCamera(Camera):
self._name = home + ' / ' + camera_name
else:
self._name = camera_name
camera_id = data.welcomedata.cameraByName(camera=camera_name,
home=home)['id']
self._unique_id = "Welcome_camera {0} - {1}".format(self._name,
camera_id)
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
camera=camera_name
)
@@ -87,31 +88,7 @@ class WelcomeCamera(Camera):
"""Return the name of this Netatmo Welcome device."""
return self._name
class WelcomeData(object):
"""Get the latest data from NetAtmo."""
def __init__(self, auth, home=None):
"""Initialize the data object."""
self.auth = auth
self.welcomedata = None
self.camera_names = []
self.home = home
def get_camera_names(self):
"""Return all module available on the API as a list."""
self.update()
if not self.home:
for home in self.welcomedata.cameras:
for camera in self.welcomedata.cameras[home].values():
self.camera_names.append(camera['name'])
else:
for camera in self.welcomedata.cameras[self.home].values():
self.camera_names.append(camera['name'])
return self.camera_names
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the NetAtmo API to update the data."""
import lnetatmo
self.welcomedata = lnetatmo.WelcomeData(self.auth)
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self._unique_id
+23 -8
View File
@@ -12,7 +12,8 @@ import shutil
import voluptuous as vol
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH)
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -35,7 +36,7 @@ DEFAULT_TIMELAPSE = 1000
DEFAULT_VERTICAL_FLIP = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FILE_PATH): cv.isfile,
vol.Optional(CONF_FILE_PATH): cv.string,
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
@@ -53,6 +54,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
def kill_raspistill(*args):
"""Kill any previously running raspistill process.."""
subprocess.Popen(['killall', 'raspistill'],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Raspberry Camera."""
if shutil.which("raspistill") is None:
@@ -75,11 +83,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
}
)
if not os.access(setup_config[CONF_FILE_PATH], os.W_OK):
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill)
try:
# Try to create an empty file (or open existing) to ensure we have
# proper permissions.
open(setup_config[CONF_FILE_PATH], 'a').close()
add_devices([RaspberryCamera(setup_config)])
except PermissionError:
_LOGGER.error("File path is not writable")
return False
add_devices([RaspberryCamera(setup_config)])
except FileNotFoundError:
_LOGGER.error("Could not create output file (missing directory?)")
return False
class RaspberryCamera(Camera):
@@ -93,9 +110,7 @@ class RaspberryCamera(Camera):
self._config = device_info
# Kill if there's raspistill instance
subprocess.Popen(['killall', 'raspistill'],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
kill_raspistill()
cmd_args = [
'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],
+302
View File
@@ -0,0 +1,302 @@
"""
Support for Synology Surveillance Station Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.synology/
"""
import asyncio
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, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Synology Camera'
DEFAULT_STREAM_ID = '0'
TIMEOUT = 5
CONF_CAMERA_NAME = 'camera_name'
CONF_STREAM_ID = 'stream_id'
QUERY_CGI = 'query.cgi'
QUERY_API = 'SYNO.API.Info'
AUTH_API = 'SYNO.API.Auth'
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
SESSION_ID = '0'
WEBAPI_PATH = '/webapi/'
AUTH_PATH = 'auth.cgi'
CAMERA_PATH = 'camera.cgi'
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
CONTENT_TYPE_HEADER = 'Content-Type'
SYNO_API_URL = '{0}{1}{2}'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
@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)
query_payload = {
'api': QUERY_API,
'method': 'Query',
'version': '1',
'query': 'SYNO.'
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
query_req = yield from websession_init.get(
syno_api_url,
params=query_payload
)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
query_resp = yield from query_req.json()
auth_path = query_resp['data'][AUTH_API]['path']
camera_api = query_resp['data'][CAMERA_API]['path']
camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path']
# cleanup
yield from query_req.release()
# Authticate to NAS to get a session id
syno_auth_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, auth_path)
session_id = yield from get_session_id(
hass,
websession_init,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
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)
camera_payload = {
'api': CAMERA_API,
'method': 'List',
'version': '1'
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
camera_req = yield from websession.get(
syno_camera_url,
params=camera_payload
)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", syno_camera_url)
return False
camera_resp = yield from camera_req.json()
cameras = camera_resp['data']['cameras']
yield from camera_req.release()
# add cameras
devices = []
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'],
snapshot_path,
streaming_path,
camera_path,
auth_path
)
devices.append(device)
yield from async_add_devices(devices)
@asyncio.coroutine
def get_session_id(hass, websession, username, password, login_url):
"""Get a session id."""
auth_payload = {
'api': AUTH_API,
'method': 'Login',
'version': '2',
'account': username,
'passwd': password,
'session': 'SurveillanceStation',
'format': 'sid'
}
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
auth_req = yield from websession.get(
login_url,
params=auth_payload
)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", login_url)
return False
auth_resp = yield from auth_req.json()
yield from auth_req.release()
return auth_resp['data']['sid']
class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera."""
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._synology_url = config.get(CONF_URL)
self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID)
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
def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
image_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._camera_path)
image_payload = {
'api': CAMERA_API,
'method': 'GetSnapshot',
'version': '1',
'cameraId': self._camera_id
}
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
response = yield from self._websession.get(
image_url,
params=image_payload
)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", image_url)
return None
image = yield from response.read()
yield from response.release()
return image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Return a MJPEG stream image response directly from the camera."""
streaming_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._streaming_path)
streaming_payload = {
'api': STREAMING_API,
'method': 'Stream',
'version': '1',
'cameraId': self._camera_id,
'format': 'mjpeg'
}
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
stream = yield from self._websession.get(
streaming_url,
params=streaming_payload
)
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)
yield from response.prepare(request)
try:
while True:
data = yield from stream.content.read(102400)
if not data:
break
response.write(data)
finally:
self.hass.async_add_job(stream.release())
yield from response.write_eof()
@property
def name(self):
"""Return the name of this device."""
return self._name
+103
View File
@@ -0,0 +1,103 @@
"""
Camera that loads a picture from a local file.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.verisure/
"""
import errno
import logging
import os
from homeassistant.components.camera import Camera
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.verisure import HUB as hub
from homeassistant.components.verisure import CONF_SMARTCAM
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Camera."""
if not int(hub.config.get(CONF_SMARTCAM, 1)):
return False
directory_path = hass.config.config_dir
if not os.access(directory_path, os.R_OK):
_LOGGER.error("file path %s is not readable", directory_path)
return False
hub.update_smartcam()
smartcams = []
smartcams.extend([
VerisureSmartcam(hass, value.deviceLabel, directory_path)
for value in hub.smartcam_status.values()])
add_devices(smartcams)
class VerisureSmartcam(Camera):
"""Local camera."""
def __init__(self, hass, device_id, directory_path):
"""Initialize Verisure File Camera component."""
super().__init__()
self._device_id = device_id
self._directory_path = directory_path
self._image = None
self._image_id = None
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
self.delete_image)
def camera_image(self):
"""Return image response."""
self.check_imagelist()
if not self._image:
_LOGGER.debug('No image to display')
return
_LOGGER.debug('Trying to open %s', self._image)
with open(self._image, 'rb') as file:
return file.read()
def check_imagelist(self):
"""Check the contents of the image list."""
hub.update_smartcam_imagelist()
if (self._device_id not in hub.smartcam_dict or
not hub.smartcam_dict[self._device_id]):
return
images = hub.smartcam_dict[self._device_id]
new_image_id = images[0]
_LOGGER.debug('self._device_id=%s, self._images=%s, '
'self._new_image_id=%s', self._device_id,
images, new_image_id)
if (new_image_id == '-1' or
self._image_id == new_image_id):
_LOGGER.debug('The image is the same, or loading image_id')
return
_LOGGER.debug('Download new image %s', new_image_id)
hub.my_pages.smartcam.download_image(self._device_id,
new_image_id,
self._directory_path)
_LOGGER.debug('Old image_id=%s', self._image_id)
self.delete_image(self)
self._image_id = new_image_id
self._image = os.path.join(self._directory_path,
'{}{}'.format(
self._image_id,
'.jpg'))
def delete_image(self, event):
"""Delete an old image."""
remove_image = os.path.join(self._directory_path,
'{}{}'.format(
self._image_id,
'.jpg'))
try:
os.remove(remove_image)
_LOGGER.debug('Deleting old image %s', remove_image)
except OSError as error:
if error.errno != errno.ENOENT:
raise
@property
def name(self):
"""Return the name of this camera."""
return hub.smartcam_status[self._device_id].location
+23 -14
View File
@@ -123,7 +123,6 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
# pylint: disable=too-many-arguments
def set_temperature(hass, temperature=None, entity_id=None,
target_temp_high=None, target_temp_low=None,
operation_mode=None):
@@ -181,7 +180,6 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
# pylint: disable=too-many-branches
def setup(hass, config):
"""Setup climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
@@ -253,7 +251,7 @@ def setup(hass, config):
kwargs[value] = convert_temperature(
temp,
hass.config.units.temperature_unit,
climate.unit_of_measurement
climate.temperature_unit
)
else:
kwargs[value] = temp
@@ -364,11 +362,14 @@ def setup(hass, config):
class ClimateDevice(Entity):
"""Representation of a climate device."""
# pylint: disable=too-many-public-methods,no-self-use
# pylint: disable=no-self-use
@property
def state(self):
"""Return the current state."""
return self.current_operation or STATE_UNKNOWN
if self.current_operation:
return self.current_operation
else:
return STATE_UNKNOWN
@property
def state_attributes(self):
@@ -398,17 +399,20 @@ class ClimateDevice(Entity):
fan_mode = self.current_fan_mode
if fan_mode is not None:
data[ATTR_FAN_MODE] = fan_mode
data[ATTR_FAN_LIST] = self.fan_list
if self.fan_list:
data[ATTR_FAN_LIST] = self.fan_list
operation_mode = self.current_operation
if operation_mode is not None:
data[ATTR_OPERATION_MODE] = operation_mode
data[ATTR_OPERATION_LIST] = self.operation_list
if self.operation_list:
data[ATTR_OPERATION_LIST] = self.operation_list
swing_mode = self.current_swing_mode
if swing_mode is not None:
data[ATTR_SWING_MODE] = swing_mode
data[ATTR_SWING_LIST] = self.swing_list
if self.swing_list:
data[ATTR_SWING_LIST] = self.swing_list
is_away = self.is_away_mode_on
if is_away is not None:
@@ -422,7 +426,12 @@ class ClimateDevice(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
"""The unit of measurement to display."""
return self.hass.config.units.temperature_unit
@property
def temperature_unit(self):
"""The unit of measurement used by the platform."""
raise NotImplementedError
@property
@@ -534,12 +543,12 @@ class ClimateDevice(Entity):
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert_temperature(7, TEMP_CELSIUS, self.unit_of_measurement)
return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert_temperature(35, TEMP_CELSIUS, self.unit_of_measurement)
return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit)
@property
def min_humidity(self):
@@ -556,10 +565,10 @@ class ClimateDevice(Entity):
if temp is None or not isinstance(temp, Number):
return temp
value = convert_temperature(temp, self.unit_of_measurement,
self.hass.config.units.temperature_unit)
value = convert_temperature(temp, self.temperature_unit,
self.unit_of_measurement)
if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
if self.unit_of_measurement is TEMP_CELSIUS:
decimal_count = 1
else:
# Users of fahrenheit generally expect integer units.
+1 -3
View File
@@ -21,11 +21,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
])
# pylint: disable=too-many-arguments, too-many-public-methods
class DemoClimate(ClimateDevice):
"""Representation of a demo climate device."""
# pylint: disable=too-many-instance-attributes
def __init__(self, name, target_temperature, unit_of_measurement,
away, current_temperature, current_fan_mode,
target_humidity, current_humidity, current_swing_mode,
@@ -59,7 +57,7 @@ class DemoClimate(ClimateDevice):
return self._name
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
+3 -7
View File
@@ -14,7 +14,7 @@ from homeassistant.components.climate import (
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS)
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
@@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
# pylint: disable=too-many-public-methods, abstract-method
class Thermostat(ClimateDevice):
"""A thermostat class for Ecobee."""
@@ -105,12 +104,9 @@ class Thermostat(ClimateDevice):
return self.thermostat['name']
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
if self.thermostat['settings']['useCelsius']:
return TEMP_CELSIUS
else:
return TEMP_FAHRENHEIT
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
+27 -12
View File
@@ -1,24 +1,38 @@
"""
Support for eq3 Bluetooth Smart thermostats.
Support for eQ-3 Bluetooth Smart thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.eq3btsmart/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE
import voluptuous as vol
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
from homeassistant.const import (
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
from homeassistant.util.temperature import convert
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['bluepy_devices==0.2.0']
CONF_MAC = 'mac'
_LOGGER = logging.getLogger(__name__)
ATTR_MODE = 'mode'
ATTR_MODE_READABLE = 'mode_readable'
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_MAC): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES):
vol.Schema({cv.string: DEVICE_SCHEMA}),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the eq3 BLE thermostats."""
"""Setup the eQ-3 BLE thermostats."""
devices = []
for name, device_cfg in config[CONF_DEVICES].items():
@@ -28,16 +42,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices)
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
# pylint: disable=import-error
class EQ3BTSmartThermostat(ClimateDevice):
"""Representation of a EQ3 Bluetooth Smart thermostat."""
"""Representation of a eQ-3 Bluetooth Smart thermostat."""
def __init__(self, _mac, _name):
"""Initialize the thermostat."""
from bluepy_devices.devices import eq3btsmart
self._name = _name
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
@property
@@ -46,7 +59,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
return self._name
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@@ -70,8 +83,10 @@ class EQ3BTSmartThermostat(ClimateDevice):
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {"mode": self._thermostat.mode,
"mode_readable": self._thermostat.mode_readable}
return {
ATTR_MODE: self._thermostat.mode,
ATTR_MODE_READABLE: self._thermostat.mode_readable,
}
@property
def min_temp(self):
@@ -62,11 +62,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
target_temp, ac_mode, min_cycle_duration)])
# pylint: disable=too-many-instance-attributes, abstract-method
class GenericThermostat(ClimateDevice):
"""Representation of a GenericThermostat device."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
"""Initialize the thermostat."""
@@ -100,7 +98,7 @@ class GenericThermostat(ClimateDevice):
return self._name
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit
@@ -147,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
@@ -160,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."""
+32 -35
View File
@@ -1,56 +1,54 @@
"""
Support for the PRT Heatmiser themostats using the V3 protocol.
See https://github.com/andylockran/heatmiserV3 for more info on the
heatmiserV3 module dependency.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.heatmiser/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
import voluptuous as vol
CONF_IPADDRESS = 'ipaddress'
CONF_PORT = 'port'
CONF_TSTATS = 'tstats'
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
from homeassistant.const import (
TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ["heatmiserV3==0.9.1"]
REQUIREMENTS = ['heatmiserV3==0.9.1']
_LOGGER = logging.getLogger(__name__)
CONF_IPADDRESS = 'ipaddress'
CONF_TSTATS = 'tstats'
TSTATS_SCHEMA = vol.Schema({
vol.Required(CONF_ID): cv.string,
vol.Required(CONF_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IPADDRESS): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TSTATS, default={}):
vol.Schema({cv.string: TSTATS_SCHEMA}),
})
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the heatmiser thermostat."""
from heatmiserV3 import heatmiser, connection
ipaddress = str(config[CONF_IPADDRESS])
port = str(config[CONF_PORT])
if ipaddress is None or port is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_IPADDRESS, CONF_PORT)
return False
ipaddress = config.get(CONF_IPADDRESS)
port = str(config.get(CONF_PORT))
tstats = config.get(CONF_TSTATS)
serport = connection.connection(ipaddress, port)
serport.open()
tstats = []
if CONF_TSTATS in config:
tstats = config[CONF_TSTATS]
if tstats is None:
_LOGGER.error("No thermostats configured.")
return False
for tstat in tstats:
for thermostat, tstat in tstats.items():
add_devices([
HeatmiserV3Thermostat(
heatmiser,
tstat.get("id"),
tstat.get("name"),
serport)
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
])
return
@@ -58,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class HeatmiserV3Thermostat(ClimateDevice):
"""Representation of a HeatmiserV3 thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, heatmiser, device, name, serport):
"""Initialize the thermostat."""
self.heatmiser = heatmiser
@@ -69,7 +66,7 @@ class HeatmiserV3Thermostat(ClimateDevice):
self._id = device
self.dcb = None
self.update()
self._target_temperature = int(self.dcb.get("roomset"))
self._target_temperature = int(self.dcb.get('roomset'))
@property
def name(self):
@@ -77,7 +74,7 @@ class HeatmiserV3Thermostat(ClimateDevice):
return self._name
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@@ -85,9 +82,9 @@ class HeatmiserV3Thermostat(ClimateDevice):
def current_temperature(self):
"""Return the current temperature."""
if self.dcb is not None:
low = self.dcb.get("floortemplow ")
high = self.dcb.get("floortemphigh")
temp = (high*256 + low)/10.0
low = self.dcb.get('floortemplow ')
high = self.dcb.get('floortemphigh')
temp = (high * 256 + low) / 10.0
self._current_temperature = temp
else:
self._current_temperature = None
@@ -36,12 +36,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
)
# pylint: disable=abstract-method
class HMThermostat(homematic.HMDevice, ClimateDevice):
"""Representation of a Homematic thermostat."""
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@@ -100,7 +100,6 @@ def _setup_us(username, password, config, add_devices):
class RoundThermostat(ClimateDevice):
"""Representation of a Honeywell Round Connected thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, device, zone_id, master, away_temp):
"""Initialize the thermostat."""
self.device = device
@@ -120,7 +119,7 @@ class RoundThermostat(ClimateDevice):
return self._name
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@@ -197,7 +196,6 @@ class RoundThermostat(ClimateDevice):
self._is_dhw = False
# pylint: disable=abstract-method
class HoneywellUSThermostat(ClimateDevice):
"""Representation of a Honeywell US Thermostat."""
@@ -217,7 +215,7 @@ class HoneywellUSThermostat(ClimateDevice):
return self._device.name
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return (TEMP_CELSIUS if self._device.temperature_unit == 'C'
else TEMP_FAHRENHEIT)
@@ -225,7 +223,6 @@ class HoneywellUSThermostat(ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
self._device.refresh()
return self._device.current_temperature
@property
@@ -276,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()
+14 -7
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):
@@ -63,23 +65,19 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
return True
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@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'))
@@ -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
@@ -37,7 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
map_sv_types, devices, add_devices, MySensorsHVAC))
# pylint: disable=too-many-arguments, too-many-public-methods
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
"""Representation of a MySensorsHVAC hvac."""
@@ -47,7 +51,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
return self.gateway.optimistic
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return (TEMP_CELSIUS
if self.gateway.metric else TEMP_FAHRENHEIT)
+89 -41
View File
@@ -5,8 +5,10 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.nest/
"""
import logging
import voluptuous as vol
import homeassistant.components.nest as nest
from homeassistant.components.nest import DATA_NEST
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
@@ -26,11 +28,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest thermostat."""
temp_unit = hass.config.units.temperature_unit
add_devices([NestThermostat(structure, device, temp_unit)
for structure, device in nest.devices()])
add_devices(
[NestThermostat(structure, device, temp_unit)
for structure, device in hass.data[DATA_NEST].devices()],
True
)
# pylint: disable=abstract-method,too-many-public-methods
class NestThermostat(ClimateDevice):
"""Representation of a Nest thermostat."""
@@ -40,51 +44,81 @@ class NestThermostat(ClimateDevice):
self.structure = structure
self.device = device
self._fan_list = [STATE_ON, STATE_AUTO]
self._operation_list = [STATE_HEAT, STATE_COOL, STATE_AUTO,
STATE_OFF]
# Not all nest devices support cooling and heating remove unused
self._operation_list = [STATE_OFF]
# Add supported nest thermostat features
if self.device.can_heat:
self._operation_list.append(STATE_HEAT)
if self.device.can_cool:
self._operation_list.append(STATE_COOL)
if self.device.can_heat and self.device.can_cool:
self._operation_list.append(STATE_AUTO)
# feature of device
self._has_humidifier = self.device.has_humidifier
self._has_dehumidifier = self.device.has_dehumidifier
self._has_fan = self.device.has_fan
# data attributes
self._away = None
self._location = None
self._name = None
self._humidity = None
self._target_humidity = None
self._target_temperature = None
self._temperature = None
self._mode = None
self._fan = None
self._away_temperature = None
@property
def name(self):
"""Return the name of the nest, if any."""
location = self.device.where
name = self.device.name
if location is None:
return name
if self._location is None:
return self._name
else:
if name == '':
return location.capitalize()
if self._name == '':
return self._location.capitalize()
else:
return location.capitalize() + '(' + name + ')'
return self._location.capitalize() + '(' + self._name + ')'
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
# Move these to Thermostat Device and make them global
return {
"humidity": self.device.humidity,
"target_humidity": self.device.target_humidity,
}
if self._has_humidifier or self._has_dehumidifier:
# Move these to Thermostat Device and make them global
return {
"humidity": self._humidity,
"target_humidity": self._target_humidity,
}
else:
# No way to control humidity not show setting
return {}
@property
def current_temperature(self):
"""Return the current temperature."""
return self.device.temperature
return self._temperature
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.device.mode == 'cool':
if self._mode == 'cool':
return STATE_COOL
elif self.device.mode == 'heat':
elif self._mode == 'heat':
return STATE_HEAT
elif self.device.mode == 'range':
elif self._mode == 'range':
return STATE_AUTO
elif self.device.mode == 'off':
elif self._mode == 'off':
return STATE_OFF
else:
return STATE_UNKNOWN
@@ -92,37 +126,37 @@ class NestThermostat(ClimateDevice):
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self.device.mode != 'range' and not self.is_away_mode_on:
return self.device.target
if self._mode != 'range' and not self.is_away_mode_on:
return self._target_temperature
else:
return None
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
if self.is_away_mode_on and self.device.away_temperature[0]:
if self.is_away_mode_on and self._away_temperature[0]:
# away_temperature is always a low, high tuple
return self.device.away_temperature[0]
if self.device.mode == 'range':
return self.device.target[0]
return self._away_temperature[0]
if self._mode == 'range':
return self._target_temperature[0]
else:
return None
@property
def target_temperature_high(self):
"""Return the upper bound temperature we try to reach."""
if self.is_away_mode_on and self.device.away_temperature[1]:
if self.is_away_mode_on and self._away_temperature[1]:
# away_temperature is always a low, high tuple
return self.device.away_temperature[1]
if self.device.mode == 'range':
return self.device.target[1]
return self._away_temperature[1]
if self._mode == 'range':
return self._target_temperature[1]
else:
return None
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self.structure.away
return self._away
def set_temperature(self, **kwargs):
"""Set new target temperature."""
@@ -130,7 +164,7 @@ class NestThermostat(ClimateDevice):
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp_low is not None and target_temp_high is not None:
if self.device.mode == 'range':
if self._mode == 'range':
temp = (target_temp_low, target_temp_high)
else:
temp = kwargs.get(ATTR_TEMPERATURE)
@@ -164,7 +198,12 @@ class NestThermostat(ClimateDevice):
@property
def current_fan_mode(self):
"""Return whether the fan is on."""
return STATE_ON if self.device.fan else STATE_AUTO
if self._has_fan:
# Return whether the fan is on
return STATE_ON if self._fan else STATE_AUTO
else:
# No Fan available so disable slider
return None
@property
def fan_list(self):
@@ -178,7 +217,7 @@ class NestThermostat(ClimateDevice):
@property
def min_temp(self):
"""Identify min_temp in Nest API or defaults if not available."""
temp = self.device.away_temperature.low
temp = self._away_temperature[0]
if temp is None:
return super().min_temp
else:
@@ -187,12 +226,21 @@ class NestThermostat(ClimateDevice):
@property
def max_temp(self):
"""Identify max_temp in Nest API or defaults if not available."""
temp = self.device.away_temperature.high
temp = self._away_temperature[1]
if temp is None:
return super().max_temp
else:
return temp
def update(self):
"""Python-nest has its own mechanism for staying up to date."""
pass
"""Cache value from Python-nest."""
self._location = self.device.where
self._name = self.device.name
self._humidity = self.device.humidity,
self._target_humidity = self.device.target_humidity,
self._temperature = self.device.temperature
self._mode = self.device.mode
self._target_temperature = self.device.target
self._fan = self.device.fan
self._away = self.structure.away
self._away_temperature = self.device.away_temperature
+177
View File
@@ -0,0 +1,177 @@
"""
Support for Netatmo Smart Thermostat.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.netatmo/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
from homeassistant.components.climate import (
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
from homeassistant.util import Throttle
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['netatmo']
_LOGGER = logging.getLogger(__name__)
CONF_RELAY = 'relay'
CONF_THERMOSTAT = 'thermostat'
DEFAULT_AWAY_TEMPERATURE = 14
# # The default offeset is 2 hours (when you use the thermostat itself)
DEFAULT_TIME_OFFSET = 7200
# # Return cached results if last scan was less then this time ago
# # NetAtmo Data is uploaded to server every hour
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_RELAY): cv.string,
vol.Optional(CONF_THERMOSTAT, default=[]):
vol.All(cv.ensure_list, [cv.string]),
})
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the NetAtmo Thermostat."""
netatmo = get_component('netatmo')
device = config.get(CONF_RELAY)
import lnetatmo
try:
data = ThermostatData(netatmo.NETATMO_AUTH, device)
for module_name in data.get_module_names():
if CONF_THERMOSTAT in config:
if config[CONF_THERMOSTAT] != [] and \
module_name not in config[CONF_THERMOSTAT]:
continue
add_callback_devices([NetatmoThermostat(data, module_name)])
except lnetatmo.NoDevice:
return None
class NetatmoThermostat(ClimateDevice):
"""Representation a Netatmo thermostat."""
def __init__(self, data, module_name, away_temp=None):
"""Initialize the sensor."""
self._data = data
self._state = None
self._name = module_name
self._target_temperature = None
self._away = None
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._target_temperature
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self._data.current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def current_operation(self):
"""Return the current state of the thermostat."""
state = self._data.thermostatdata.relay_cmd
if state == 0:
return STATE_IDLE
elif state == 100:
return STATE_HEAT
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away
def turn_away_mode_on(self):
"""Turn away on."""
mode = "away"
temp = None
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
self._away = True
self.update_ha_state()
def turn_away_mode_off(self):
"""Turn away off."""
mode = "program"
temp = None
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
self._away = False
self.update_ha_state()
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
"""Set new target temperature for 2 hours."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
mode = "manual"
self._data.thermostatdata.setthermpoint(
mode, temperature, endTimeOffset)
self._target_temperature = temperature
self._away = False
self.update_ha_state()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from NetAtmo API and updates the states."""
self._data.update()
self._target_temperature = self._data.thermostatdata.setpoint_temp
self._away = self._data.setpoint_mode == 'away'
class ThermostatData(object):
"""Get the latest data from Netatmo."""
def __init__(self, auth, device=None):
"""Initialize the data object."""
self.auth = auth
self.thermostatdata = None
self.module_names = []
self.device = device
self.current_temperature = None
self.target_temperature = None
self.setpoint_mode = None
# self.operation =
def get_module_names(self):
"""Return all module available on the API as a list."""
self.update()
if not self.device:
for device in self.thermostatdata.modules:
for module in self.thermostatdata.modules[device].values():
self.module_names.append(module['module_name'])
else:
for module in self.thermostatdata.modules[self.device].values():
self.module_names.append(module['module_name'])
return self.module_names
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the NetAtmo API to update the data."""
import lnetatmo
self.thermostatdata = lnetatmo.ThermostatData(self.auth)
self.target_temperature = self.thermostatdata.setpoint_temp
self.setpoint_mode = self.thermostatdata.setpoint_mode
self.current_temperature = self.thermostatdata.temp
@@ -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.3.1']
REQUIREMENTS = ['proliphix==0.4.1']
ATTR_FAN = 'fan'
@@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ProliphixThermostat(pdp)])
# pylint: disable=abstract-method
class ProliphixThermostat(ClimateDevice):
"""Representation a Proliphix thermostat."""
@@ -69,7 +68,7 @@ class ProliphixThermostat(ClimateDevice):
}
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
+15 -12
View File
@@ -6,7 +6,6 @@ https://home-assistant.io/components/climate.radiotherm/
"""
import datetime
import logging
from urllib.error import URLError
import voluptuous as vol
@@ -52,14 +51,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try:
tstat = radiotherm.get_thermostat(host)
tstats.append(RadioThermostat(tstat, hold_temp))
except (URLError, OSError):
except OSError:
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
host)
add_devices(tstats)
# pylint: disable=abstract-method
class RadioThermostat(ClimateDevice):
"""Representation of a Radio Thermostat."""
@@ -71,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]
@@ -81,7 +81,7 @@ class RadioThermostat(ClimateDevice):
return self._name
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@@ -89,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
@@ -117,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:
@@ -132,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:
@@ -156,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
+12 -4
View File
@@ -8,7 +8,10 @@ import logging
from homeassistant.util import convert
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE
from homeassistant.const import (
TEMP_FAHRENHEIT,
TEMP_CELSIUS,
ATTR_TEMPERATURE)
from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
@@ -28,7 +31,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
device in VERA_DEVICES['climate'])
# pylint: disable=abstract-method
class VeraThermostat(VeraDevice, ClimateDevice):
"""Representation of a Vera Thermostat."""
@@ -93,9 +95,15 @@ class VeraThermostat(VeraDevice, ClimateDevice):
self._state = self.vera_device.get_hvac_mode()
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
vera_temp_units = (
self.vera_device.vera_controller.temperature_units)
if vera_temp_units == 'F':
return TEMP_FAHRENHEIT
return TEMP_CELSIUS
@property
def current_temperature(self):
+331
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
+69 -110
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)
@@ -69,30 +47,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info, zwave.NETWORK)
# pylint: disable=abstract-method
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Represents a ZWave Climate device."""
"""Representation of a Z-Wave Climate device."""
# pylint: disable=too-many-instance-attributes
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
self._current_operation = None
self._operation_list = None
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(
@@ -107,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):
@@ -126,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)
@@ -150,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:
@@ -162,30 +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()):
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
@@ -209,7 +192,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
return self._swing_list
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
if self._unit == 'C':
return TEMP_CELSIUS
@@ -244,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):
@@ -323,3 +272,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
value.index == 33:
value.data = bytes(swing_mode, 'utf-8')
break
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
data = super().device_state_attributes
if self._operating_state:
data[ATTR_OPERATING_STATE] = self._operating_state,
if self._fan_state:
data[ATTR_FAN_STATE] = self._fan_state
return data
+7 -6
View File
@@ -8,7 +8,8 @@ the user has submitted configuration information.
"""
import logging
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
ATTR_ENTITY_PICTURE
from homeassistant.helpers.entity import generate_entity_id
_INSTANCES = {}
@@ -33,10 +34,10 @@ STATE_CONFIGURE = 'configure'
STATE_CONFIGURED = 'configured'
# pylint: disable=too-many-arguments
def request_config(
hass, name, callback, description=None, description_image=None,
submit_caption=None, fields=None, link_name=None, link_url=None):
submit_caption=None, fields=None, link_name=None, link_url=None,
entity_picture=None):
"""Create a new request for configuration.
Will return an ID to be used for sequent calls.
@@ -46,7 +47,7 @@ def request_config(
request_id = instance.request_config(
name, callback,
description, description_image, submit_caption,
fields, link_name, link_url)
fields, link_name, link_url, entity_picture)
_REQUESTS[request_id] = instance
@@ -100,11 +101,10 @@ class Configurator(object):
hass.services.register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
# pylint: disable=too-many-arguments
def request_config(
self, name, callback,
description, description_image, submit_caption,
fields, link_name, link_url):
fields, link_name, link_url, entity_picture):
"""Setup a request for configuration."""
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
@@ -119,6 +119,7 @@ class Configurator(object):
ATTR_CONFIGURE_ID: request_id,
ATTR_FIELDS: fields,
ATTR_FRIENDLY_NAME: name,
ATTR_ENTITY_PICTURE: entity_picture,
}
data.update({
+1 -1
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'
@@ -38,7 +38,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for device_name, device_config in devices.items():
value_template = device_config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass
if value_template is not None:
value_template.hass = hass
covers.append(
CommandCover(
@@ -59,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(covers)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class CommandCover(CoverDevice):
"""Representation a command line cover."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, command_open, command_close, command_stop,
command_state, value_template):
"""Initialize the cover."""
+1 -1
View File
@@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoCover(CoverDevice):
"""Representation of a demo cover."""
# pylint: disable=no-self-use, too-many-instance-attributes
# pylint: disable=no-self-use
def __init__(self, hass, name, position=None, tilt_position=None):
"""Initialize the cover."""
self.hass = hass
+275
View File
@@ -0,0 +1,275 @@
"""
Platform for the garadget cover component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/garadget/
"""
import logging
import voluptuous as vol
import requests
from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.const import CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD,\
CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN,\
CONF_COVERS
import homeassistant.helpers.config_validation as cv
DEFAULT_NAME = 'Garadget'
ATTR_SIGNAL_STRENGTH = "wifi signal strength (dB)"
ATTR_TIME_IN_STATE = "time in state"
ATTR_SENSOR_STRENGTH = "sensor reflection rate"
ATTR_AVAILABLE = "available"
STATE_OPENING = "opening"
STATE_CLOSING = "closing"
STATE_STOPPED = "stopped"
STATE_OFFLINE = "offline"
STATES_MAP = {
"open": STATE_OPEN,
"opening": STATE_OPENING,
"closed": STATE_CLOSED,
"closing": STATE_CLOSING,
"stopped": STATE_STOPPED
}
# Validation of the user's configuration
COVER_SCHEMA = vol.Schema({
vol.Optional(CONF_DEVICE): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
})
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo covers."""
covers = []
devices = config.get(CONF_COVERS, {})
_LOGGER.debug(devices)
for device_id, device_config in devices.items():
args = {
"name": device_config.get(CONF_NAME),
"device_id": device_config.get(CONF_DEVICE, device_id),
"username": device_config.get(CONF_USERNAME),
"password": device_config.get(CONF_PASSWORD),
"access_token": device_config.get(CONF_ACCESS_TOKEN)
}
covers.append(GaradgetCover(hass, args))
add_devices(covers)
class GaradgetCover(CoverDevice):
"""Representation of a demo cover."""
# pylint: disable=no-self-use, too-many-instance-attributes
def __init__(self, hass, args):
"""Initialize the cover."""
self.particle_url = 'https://api.particle.io'
self.hass = hass
self._name = args['name']
self.device_id = args['device_id']
self.access_token = args['access_token']
self.obtained_token = False
self._username = args['username']
self._password = args['password']
self._state = STATE_UNKNOWN
self.time_in_state = None
self.signal = None
self.sensor = None
self._unsub_listener_cover = None
self._available = True
if self.access_token is None:
self.access_token = self.get_token()
self._obtained_token = True
# Lets try to get the configured name if not provided.
try:
if self._name is None:
doorconfig = self._get_variable("doorConfig")
if doorconfig["nme"] is not None:
self._name = doorconfig["nme"]
self.update()
except requests.exceptions.ConnectionError as ex:
_LOGGER.error('Unable to connect to server: %(reason)s',
dict(reason=ex))
self._state = STATE_OFFLINE
self._available = False
self._name = DEFAULT_NAME
except KeyError as ex:
_LOGGER.warning('Garadget device %(device)s seems to be offline',
dict(device=self.device_id))
self._name = DEFAULT_NAME
self._state = STATE_OFFLINE
self._available = False
def __del__(self):
"""Try to remove token."""
if self._obtained_token is True:
if self.access_token is not None:
self.remove_token()
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def should_poll(self):
"""No polling needed for a demo cover."""
return True
@property
def available(self):
"""Return True if entity is available."""
return self._available
@property
def device_state_attributes(self):
"""Return the device state attributes."""
data = {}
if self.signal is not None:
data[ATTR_SIGNAL_STRENGTH] = self.signal
if self.time_in_state is not None:
data[ATTR_TIME_IN_STATE] = self.time_in_state
if self.sensor is not None:
data[ATTR_SENSOR_STRENGTH] = self.sensor
if self.access_token is not None:
data[CONF_ACCESS_TOKEN] = self.access_token
return data
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._state == STATE_UNKNOWN:
return None
else:
return self._state == STATE_CLOSED
def get_token(self):
"""Get new token for usage during this session."""
args = {
'grant_type': 'password',
'username': self._username,
'password': self._password
}
url = '{}/oauth/token'.format(self.particle_url)
ret = requests.post(url,
auth=('particle', 'particle'),
data=args)
return ret.json()['access_token']
def remove_token(self):
"""Remove authorization token from API."""
ret = requests.delete('{}/v1/access_tokens/{}'.format(
self.particle_url,
self.access_token),
auth=(self._username, self._password))
return ret.text
def _start_watcher(self, command):
"""Start watcher."""
_LOGGER.debug("Starting Watcher for command: %s ", command)
if self._unsub_listener_cover is None:
self._unsub_listener_cover = track_utc_time_change(
self.hass, self._check_state)
def _check_state(self, now):
"""Check the state of the service during an operation."""
self.update()
self.update_ha_state()
def close_cover(self):
"""Close the cover."""
if self._state not in ["close", "closing"]:
ret = self._put_command("setState", "close")
self._start_watcher('close')
return ret.get('return_value') == 1
def open_cover(self):
"""Open the cover."""
if self._state not in ["open", "opening"]:
ret = self._put_command("setState", "open")
self._start_watcher('open')
return ret.get('return_value') == 1
def stop_cover(self):
"""Stop the door where it is."""
if self._state not in ["stopped"]:
ret = self._put_command("setState", "stop")
self._start_watcher('stop')
return ret['return_value'] == 1
def update(self):
"""Get updated status from API."""
try:
status = self._get_variable("doorStatus")
_LOGGER.debug("Current Status: %s", status['status'])
self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN)
self.time_in_state = status['time']
self.signal = status['signal']
self.sensor = status['sensor']
self._availble = True
except requests.exceptions.ConnectionError as ex:
_LOGGER.error('Unable to connect to server: %(reason)s',
dict(reason=ex))
self._state = STATE_OFFLINE
except KeyError as ex:
_LOGGER.warning('Garadget device %(device)s seems to be offline',
dict(device=self.device_id))
self._state = STATE_OFFLINE
if self._state not in [STATE_CLOSING, STATE_OPENING]:
if self._unsub_listener_cover is not None:
self._unsub_listener_cover()
self._unsub_listener_cover = None
def _get_variable(self, var):
"""Get latest status."""
url = '{}/v1/devices/{}/{}?access_token={}'.format(
self.particle_url,
self.device_id,
var,
self.access_token,
)
ret = requests.get(url)
result = {}
for pairs in ret.json()['result'].split('|'):
key = pairs.split('=')
result[key[0]] = key[1]
return result
def _put_command(self, func, arg=None):
"""Send commands to API."""
params = {'access_token': self.access_token}
if arg:
params['command'] = arg
url = '{}/v1/devices/{}/{}'.format(
self.particle_url,
self.device_id,
func)
ret = requests.post(url, data=params)
return ret.json()
@@ -31,7 +31,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
)
# pylint: disable=abstract-method
class HMCover(homematic.HMDevice, CoverDevice):
"""Represents a Homematic Cover in Home Assistant."""
+7 -6
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 (
@@ -67,7 +68,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttCover(CoverDevice):
"""Representation of a cover that can be controlled using MQTT."""
@@ -90,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
+6 -1
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 = {
-1
View File
@@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update)
# pylint: disable=abstract-method
class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice):
"""Representation of an rfxtrx cover."""
@@ -63,11 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(covers)
# pylint: disable=abstract-method
class RPiGPIOCover(CoverDevice):
"""Representation of a Raspberry GPIO cover."""
# pylint: disable=too-many-arguments
def __init__(self, name, relay_pin, state_pin, state_pull_mode,
relay_time):
"""Initialize the cover."""
@@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(covers)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class SCSGateCover(CoverDevice):
"""Representation of SCSGate cover."""
+2 -3
View File
@@ -15,14 +15,13 @@ DEPENDENCIES = ['vera']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Vera covers."""
add_devices_callback(
add_devices(
VeraCover(device, VERA_CONTROLLER) for
device in VERA_DEVICES['cover'])
# pylint: disable=abstract-method
class VeraCover(VeraDevice, CoverDevice):
"""Represents a Vera Cover in Home Assistant."""
+8 -8
View File
@@ -32,14 +32,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \
and value.index == 0:
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
and value.index == 0):
value.set_change_verified(False)
add_devices([ZwaveRollershutter(value)])
elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \
node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
if value.type != zwave.const.TYPE_BOOL and \
value.genre != zwave.const.GENRE_USER:
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)])
@@ -122,7 +122,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Open' or value.command_class == \
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Down':
'Up':
self._lozwmgr.pressButton(value.value_id)
break
@@ -132,7 +132,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == \
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Up' or value.command_class == \
'Down' or value.command_class == \
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Close':
self._lozwmgr.pressButton(value.value_id)
+13 -10
View File
@@ -17,6 +17,7 @@ DOMAIN = 'demo'
COMPONENTS_WITH_DEMO_PLATFORM = [
'alarm_control_panel',
'binary_sensor',
'calendar',
'camera',
'climate',
'cover',
@@ -67,31 +68,33 @@ def setup(hass, config):
lights = sorted(hass.states.entity_ids('light'))
switches = sorted(hass.states.entity_ids('switch'))
media_players = sorted(hass.states.entity_ids('media_player'))
group.Group(hass, 'living room', [
group.Group.create_group(hass, 'living room', [
lights[1], switches[0], 'input_select.living_room_preset',
'rollershutter.living_room_window', media_players[1],
'scene.romantic_lights'])
group.Group(hass, 'bedroom', [
group.Group.create_group(hass, 'bedroom', [
lights[0], switches[1], media_players[0],
'input_slider.noise_allowance'])
group.Group(hass, 'kitchen', [
group.Group.create_group(hass, 'kitchen', [
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
group.Group(hass, 'doors', [
group.Group.create_group(hass, 'doors', [
'lock.front_door', 'lock.kitchen_door',
'garage_door.right_garage_door', 'garage_door.left_garage_door'])
group.Group(hass, 'automations', [
group.Group.create_group(hass, 'automations', [
'input_select.who_cooks', 'input_boolean.notify', ])
group.Group(hass, 'people', [
group.Group.create_group(hass, 'people', [
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
'device_tracker.demo_paulus'])
group.Group(hass, 'thermostats', [
group.Group.create_group(hass, 'thermostats', [
'thermostat.nest', 'thermostat.thermostat'])
group.Group(hass, 'downstairs', [
group.Group.create_group(hass, 'downstairs', [
'group.living_room', 'group.kitchen',
'scene.romantic_lights', 'rollershutter.kitchen_window',
'rollershutter.living_room_window', 'group.doors', 'thermostat.nest',
'rollershutter.living_room_window', 'group.doors',
'thermostat.nest',
], view=True)
group.Group(hass, 'Upstairs', [
group.Group.create_group(hass, 'Upstairs', [
'thermostat.thermostat', 'group.bedroom',
], view=True)
@@ -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
@@ -41,7 +42,6 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-locals
def setup(hass, config):
"""The triggers to turn lights on or off based on device presence."""
logger = logging.getLogger(__name__)
@@ -80,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.
@@ -105,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
@@ -123,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
@@ -137,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?
@@ -150,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
@@ -159,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
@@ -167,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
@@ -4,18 +4,17 @@ Provide functionality to keep track of devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/device_tracker/
"""
# pylint: disable=too-many-instance-attributes, too-many-arguments
# pylint: disable=too-many-locals
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
@@ -25,12 +24,14 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
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']
@@ -46,6 +47,7 @@ CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = 180
CONF_SCAN_INTERVAL = 'interval_seconds'
DEFAULT_SCAN_INTERVAL = 12
@@ -53,6 +55,8 @@ DEFAULT_SCAN_INTERVAL = 12
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
EVENT_NEW_DEVICE = 'device_tracker_new_device'
SERVICE_SEE = 'see'
ATTR_MAC = 'mac'
@@ -65,15 +69,11 @@ ATTR_ATTRIBUTES = 'attributes'
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds
}, extra=vol.ALLOW_EXTRA)
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
vol.Schema({
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(
CONF_CONSIDER_HOME, default=timedelta(seconds=180)): vol.All(
cv.time_period, cv.positive_timedelta)
}, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA)
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME,
default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All(
cv.time_period, cv.positive_timedelta)
})
DISCOVERY_PLATFORMS = {
SERVICE_NETGEAR: 'netgear',
@@ -88,7 +88,6 @@ def is_on(hass: HomeAssistantType, entity_id: str=None):
return hass.states.is_state(entity, STATE_HOME)
# pylint: disable=too-many-arguments
def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=None,
@@ -103,79 +102,97 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
(ATTR_GPS_ACCURACY, gps_accuracy),
(ATTR_BATTERY, battery)) if value is not None}
if attributes:
for key, value in attributes:
data[key] = value
data[ATTR_ATTRIBUTES] = attributes
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_SCHEMA(config).get(DOMAIN, [])
conf = config.get(DOMAIN, [])
except vol.Invalid as ex:
log_exception(ex, DOMAIN, config)
async_log_exception(ex, DOMAIN, config, hass)
return False
else:
conf = conf[0] if len(conf) > 0 else {}
consider_home = conf[CONF_CONSIDER_HOME]
track_new = conf[CONF_TRACK_NEW]
devices = load_config(yaml_path, hass, consider_home)
consider_home = conf.get(CONF_CONSIDER_HOME,
timedelta(seconds=DEFAULT_CONSIDER_HOME))
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
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
@@ -189,79 +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
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)
device.seen(host_name, location_name, gps, gps_accuracy, battery,
attributes)
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
# 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)
# 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
def setup_group(self):
"""Initialize group for all tracked devices."""
yield from device.async_seen(host_name, location_name, gps,
gps_accuracy, battery, attributes)
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 is a coroutine.
"""
entity_ids = (dev.entity_id for dev in self.devices.values()
if dev.track)
self.group = group.Group(
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):
@@ -348,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
@@ -359,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:
@@ -398,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,
@@ -411,13 +487,19 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
})
try:
result = []
devices = load_yaml_config_file(path)
try:
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 []
for dev_id, device in devices.items():
try:
device = dev_schema(device)
device['dev_id'] = cv.slug(dev_id)
device['dev_id'] = cv.slugify(dev_id)
except vol.Invalid as exp:
log_exception(exp, dev_id, devices)
async_log_exception(exp, dev_id, devices, hass)
else:
result.append(Device(hass, **device))
return result
@@ -426,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
@@ -436,36 +522,41 @@ 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):
"""Add device to YAML configuration file."""
with open(path, 'a') as out:
device = {device.dev_id: {
'name': device.name,
'mac': device.mac,
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide
}}
out.write('\n')
out.write('{}:\n'.format(device.dev_id))
for key, value in (('name', device.name), ('mac', device.mac),
('picture', device.config_picture),
('track', 'yes' if device.track else 'no'),
(CONF_AWAY_HIDE,
'yes' if device.away_hide else 'no')):
out.write(' {}: {}\n'.format(key, '' if value is None else value))
out.write(dump(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())
@@ -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"])
@@ -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,15 +93,14 @@ 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):
"""This class queries a router running ASUSWRT firmware."""
# pylint: disable=too-many-instance-attributes, too-many-branches
# Eighth attribute needed for mode (AP mode vs router mode)
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
@@ -157,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
@@ -186,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
@@ -215,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
@@ -279,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'))
@@ -60,8 +60,6 @@ def setup_scanner(hass, config: dict, see):
return True
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-few-public-methods
class AutomaticDeviceScanner(object):
"""A class representing an Automatic device."""
@@ -0,0 +1,83 @@
"""
Support for French FAI Bouygues Bbox routers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.bbox/
"""
from collections import namedtuple
import logging
from datetime import timedelta
import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.util import Throttle
REQUIREMENTS = ['pybbox==0.0.5-alpha']
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
def get_scanner(hass, config):
"""Validate the configuration and return a Bbox scanner."""
scanner = BboxDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
class BboxDeviceScanner(object):
"""This class scans for devices connected to the bbox."""
def __init__(self, config):
"""Initialize the scanner."""
self.last_results = [] # type: List[Device]
self.success_init = self._update_info()
_LOGGER.info("Bbox scanner initialized")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [device.mac for device in self.last_results]
def get_device_name(self, mac):
"""Return the name of the given device or None if we don't know."""
filter_named = [device.name for device in self.last_results if
device.mac == mac]
if filter_named:
return filter_named[0]
else:
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Check the bbox for devices.
Returns boolean if scanning successful.
"""
_LOGGER.info("Scanning...")
import pybbox
box = pybbox.Bbox()
result = box.get_all_connected_devices()
now = dt_util.now()
last_results = []
for device in result:
if device['active'] != 1:
continue
last_results.append(
Device(device['macaddress'], device['hostname'],
device['ipaddress'], now))
self.last_results = last_results
_LOGGER.info("Bbox scan successful")
return True
@@ -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()
@@ -35,12 +35,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a DD-WRT scanner."""
scanner = DdWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
try:
return DdWrtDeviceScanner(config[DOMAIN])
except ConnectionError:
return None
# pylint: disable=too-many-instance-attributes
class DdWrtDeviceScanner(object):
"""This class queries a wireless router running DD-WRT firmware."""
@@ -53,13 +53,13 @@ class DdWrtDeviceScanner(object):
self.lock = threading.Lock()
self.last_results = {}
self.mac2name = {}
# Test the router is accessible
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
data = self.get_ddwrt_data(url)
self.success_init = data is not None
if not data:
raise ConnectionError('Cannot connect to DD-Wrt router')
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
@@ -83,14 +83,15 @@ class DdWrtDeviceScanner(object):
if not dhcp_leases:
return None
# Remove leading and trailing single quotes.
cleaned_str = dhcp_leases.strip().strip('"')
elements = cleaned_str.split('","')
num_clients = int(len(elements)/5)
# Remove leading and trailing quotes and spaces
cleaned_str = dhcp_leases.replace(
"\"", "").replace("\'", "").replace(" ", "")
elements = cleaned_str.split(',')
num_clients = int(len(elements) / 5)
self.mac2name = {}
for idx in range(0, num_clients):
# This is stupid but the data is a single array
# every 5 elements represents one hosts, the MAC
# The data is a single array
# every 5 elements represents one host, the MAC
# is the third element and the name is the first.
mac_index = (idx * 5) + 2
if mac_index < len(elements):
@@ -105,9 +106,6 @@ class DdWrtDeviceScanner(object):
Return boolean if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info('Checking ARP')
@@ -123,11 +121,8 @@ class DdWrtDeviceScanner(object):
if not active_clients:
return False
# This is really lame, instead of using JSON the DD-WRT UI
# uses its own data format for some reason and then
# regex's out values so I guess I have to do the same,
# LAME!!!
# The DD-WRT UI uses its own data format and then
# regex's out values so this is done here too
# Remove leading and trailing single quotes.
clean_str = active_clients.strip().strip("'")
elements = clean_str.split("','")
@@ -38,7 +38,6 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class FritzBoxScanner(object):
"""This class queries a FRITZ!Box router."""
@@ -79,7 +78,7 @@ class FritzBoxScanner(object):
self._update_info()
active_hosts = []
for known_host in self.last_results:
if known_host['status'] == '1':
if known_host['status'] == '1' and known_host.get('mac'):
active_hosts.append(known_host['mac'])
return active_hosts
+398 -71
View File
@@ -1,100 +1,427 @@
"""
Support for iCloud connected devices.
Platform that supports scanning iCloud.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.icloud/
"""
import logging
import random
import os
import voluptuous as vol
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_START)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT)
from homeassistant.components.zone import active_zone
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
PLATFORM_SCHEMA)
import homeassistant.util.dt as dt_util
from homeassistant.util.location import distance
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyicloud==0.9.1']
CONF_INTERVAL = 'interval'
KEEPALIVE_INTERVAL = 4
CONF_IGNORED_DEVICES = 'ignored_devices'
CONF_ACCOUNTNAME = 'account_name'
# entity attributes
ATTR_ACCOUNTNAME = 'account_name'
ATTR_INTERVAL = 'interval'
ATTR_DEVICENAME = 'device_name'
ATTR_BATTERY = 'battery'
ATTR_DISTANCE = 'distance'
ATTR_DEVICESTATUS = 'device_status'
ATTR_LOWPOWERMODE = 'low_power_mode'
ATTR_BATTERYSTATUS = 'battery_status'
ICLOUDTRACKERS = {}
_CONFIGURING = {}
DEVICESTATUSSET = ['features', 'maxMsgChar', 'darkWake', 'fmlyShare',
'deviceStatus', 'remoteLock', 'activationLocked',
'deviceClass', 'id', 'deviceModel', 'rawDeviceModel',
'passcodeLength', 'canWipeAfterLock', 'trackingInfo',
'location', 'msg', 'batteryLevel', 'remoteWipe',
'thisDevice', 'snd', 'prsId', 'wipeInProgress',
'lowPowerMode', 'lostModeEnabled', 'isLocating',
'lostModeCapable', 'mesg', 'name', 'batteryStatus',
'lockedTimestamp', 'lostTimestamp', 'locationCapable',
'deviceDisplayName', 'lostDevice', 'deviceColor',
'wipedTimestamp', 'modelDisplayName', 'locationEnabled',
'isMac', 'locFoundEnabled']
DEVICESTATUSCODES = {'200': 'online', '201': 'offline', '203': 'pending',
'204': 'unregistered'}
SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]),
vol.Optional(ATTR_DEVICENAME): cv.slugify,
vol.Optional(ATTR_INTERVAL): cv.positive_int,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): vol.Coerce(str),
vol.Required(CONF_PASSWORD): vol.Coerce(str),
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
vol.Range(min=1))
})
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
})
def setup_scanner(hass, config, see):
"""Setup the iCloud Scanner."""
from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException
from pyicloud.exceptions import PyiCloudNoDevicesException
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
def setup_scanner(hass, config: dict, see):
"""Set up the iCloud Scanner."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0]))
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
icloudaccount = Icloud(hass, username, password, account, see)
try:
_LOGGER.info('Logging into iCloud Account')
# Attempt the login to iCloud
api = PyiCloudService(username, password, verify=True)
except PyiCloudFailedLoginException as error:
_LOGGER.exception('Error logging into iCloud Service: %s', error)
if icloudaccount.api is not None:
ICLOUDTRACKERS[account] = icloudaccount
else:
_LOGGER.error("No ICLOUDTRACKERS added")
return False
def keep_alive(now):
"""Keep authenticating iCloud connection.
def lost_iphone(call):
"""Call the lost iphone function if the device is found."""
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
devicename = call.data.get(ATTR_DEVICENAME)
for account in accounts:
if account in ICLOUDTRACKERS:
ICLOUDTRACKERS[account].lost_iphone(devicename)
hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone,
schema=SERVICE_SCHEMA)
The session timeouts if we are not using it so we
have to re-authenticate & this will send an email.
"""
api.authenticate()
_LOGGER.info("Authenticate against iCloud")
def update_icloud(call):
"""Call the update function of an icloud account."""
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
devicename = call.data.get(ATTR_DEVICENAME)
for account in accounts:
if account in ICLOUDTRACKERS:
ICLOUDTRACKERS[account].update_icloud(devicename)
hass.services.register(DOMAIN, 'icloud_update', update_icloud,
schema=SERVICE_SCHEMA)
seen_devices = {}
def reset_account_icloud(call):
"""Reset an icloud account."""
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
for account in accounts:
if account in ICLOUDTRACKERS:
ICLOUDTRACKERS[account].reset_account_icloud()
hass.services.register(DOMAIN, 'icloud_reset_account',
reset_account_icloud, schema=SERVICE_SCHEMA)
def update_icloud(now):
"""Authenticate against iCloud and scan for devices."""
try:
keep_alive(None)
# Loop through every device registered with the iCloud account
for device in api.devices:
status = device.status()
dev_id = slugify(status['name'].replace(' ', '', 99))
def setinterval(call):
"""Call the update function of an icloud account."""
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
interval = call.data.get(ATTR_INTERVAL)
devicename = call.data.get(ATTR_DEVICENAME)
for account in accounts:
if account in ICLOUDTRACKERS:
ICLOUDTRACKERS[account].setinterval(interval, devicename)
# An entity will not be created by see() when track=false in
# 'known_devices.yaml', but we need to see() it at least once
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id))
if entity is None and dev_id in seen_devices:
continue
seen_devices[dev_id] = True
location = device.location()
# If the device has a location add it. If not do nothing
if location:
see(
dev_id=dev_id,
host_name=status['name'],
gps=(location['latitude'], location['longitude']),
battery=status['batteryLevel']*100,
gps_accuracy=location['horizontalAccuracy']
)
except PyiCloudNoDevicesException:
_LOGGER.info('No iCloud Devices found!')
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
# Schedule keepalives between the updates
keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
if x not in update_minutes)
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
hass.services.register(DOMAIN, 'icloud_set_interval', setinterval,
schema=SERVICE_SCHEMA)
# Tells the bootstrapper that the component was successfully initialized
return True
class Icloud(object):
"""Represent an icloud account in Home Assistant."""
def __init__(self, hass, username, password, name, see):
"""Initialize an iCloud account."""
self.hass = hass
self.username = username
self.password = password
self.api = None
self.accountname = name
self.devices = {}
self.seen_devices = {}
self._overridestates = {}
self._intervals = {}
self.see = see
self._trusted_device = None
self._verification_code = None
self._attrs = {}
self._attrs[ATTR_ACCOUNTNAME] = name
self.reset_account_icloud()
randomseconds = random.randint(10, 59)
track_utc_time_change(
self.hass, self.keep_alive,
second=randomseconds
)
def reset_account_icloud(self):
"""Reset an icloud account."""
from pyicloud import PyiCloudService
from pyicloud.exceptions import (
PyiCloudFailedLoginException, PyiCloudNoDevicesException)
icloud_dir = self.hass.config.path('icloud')
if not os.path.exists(icloud_dir):
os.makedirs(icloud_dir)
try:
self.api = PyiCloudService(
self.username, self.password,
cookie_directory=icloud_dir,
verify=True)
except PyiCloudFailedLoginException as error:
self.api = None
_LOGGER.error('Error logging into iCloud Service: %s', error)
return
try:
self.devices = {}
self._overridestates = {}
self._intervals = {}
for device in self.api.devices:
status = device.status(DEVICESTATUSSET)
devicename = slugify(status['name'].replace(' ', '', 99))
if devicename not in self.devices:
self.devices[devicename] = device
self._intervals[devicename] = 1
self._overridestates[devicename] = None
except PyiCloudNoDevicesException:
_LOGGER.error('No iCloud Devices found!')
def icloud_trusted_device_callback(self, callback_data):
"""The trusted device is chosen."""
self._trusted_device = int(callback_data.get('0', '0'))
self._trusted_device = self.api.trusted_devices[self._trusted_device]
if self.accountname in _CONFIGURING:
request_id = _CONFIGURING.pop(self.accountname)
configurator = get_component('configurator')
configurator.request_done(request_id)
def icloud_need_trusted_device(self):
"""We need a trusted device."""
configurator = get_component('configurator')
if self.accountname in _CONFIGURING:
return
devicesstring = ''
devices = self.api.trusted_devices
for i, device in enumerate(devices):
devicesstring += "{}: {};".format(i, device.get('deviceName'))
_CONFIGURING[self.accountname] = configurator.request_config(
self.hass, 'iCloud {}'.format(self.accountname),
self.icloud_trusted_device_callback,
description=(
'Please choose your trusted device by entering'
' the index from this list: ' + devicesstring),
entity_picture="/static/images/config_icloud.png",
submit_caption='Confirm',
fields=[{'id': '0'}]
)
def icloud_verification_callback(self, callback_data):
"""The trusted device is chosen."""
self._verification_code = callback_data.get('0')
if self.accountname in _CONFIGURING:
request_id = _CONFIGURING.pop(self.accountname)
configurator = get_component('configurator')
configurator.request_done(request_id)
def icloud_need_verification_code(self):
"""We need a verification code."""
configurator = get_component('configurator')
if self.accountname in _CONFIGURING:
return
if self.api.send_verification_code(self._trusted_device):
self._verification_code = 'waiting'
_CONFIGURING[self.accountname] = configurator.request_config(
self.hass, 'iCloud {}'.format(self.accountname),
self.icloud_verification_callback,
description=('Please enter the validation code:'),
entity_picture="/static/images/config_icloud.png",
submit_caption='Confirm',
fields=[{'code': '0'}]
)
def keep_alive(self, now):
"""Keep the api alive."""
from pyicloud.exceptions import PyiCloud2FARequiredError
if self.api is None:
self.reset_account_icloud()
if self.api is None:
return
if self.api.requires_2fa:
try:
self.api.authenticate()
except PyiCloud2FARequiredError:
if self._trusted_device is None:
self.icloud_need_trusted_device()
return
if self._verification_code is None:
self.icloud_need_verification_code()
return
if self._verification_code == 'waiting':
return
if self.api.validate_verification_code(
self._trusted_device, self._verification_code):
self._verification_code = None
else:
self.api.authenticate()
currentminutes = dt_util.now().hour * 60 + dt_util.now().minute
for devicename in self.devices:
interval = self._intervals.get(devicename, 1)
if ((currentminutes % interval == 0) or
(interval > 10 and
currentminutes % interval in [2, 4])):
self.update_device(devicename)
def determine_interval(self, devicename, latitude, longitude, battery):
"""Calculate new interval."""
distancefromhome = None
zone_state = self.hass.states.get('zone.home')
zone_state_lat = zone_state.attributes['latitude']
zone_state_long = zone_state.attributes['longitude']
distancefromhome = distance(latitude, longitude, zone_state_lat,
zone_state_long)
distancefromhome = round(distancefromhome / 1000, 1)
currentzone = active_zone(self.hass, latitude, longitude)
if ((currentzone is not None and
currentzone == self._overridestates.get(devicename)) or
(currentzone is None and
self._overridestates.get(devicename) == 'away')):
return
self._overridestates[devicename] = None
if currentzone is not None:
self._intervals[devicename] = 30
return
if distancefromhome is None:
return
if distancefromhome > 25:
self._intervals[devicename] = round(distancefromhome / 2, 0)
elif distancefromhome > 10:
self._intervals[devicename] = 5
else:
self._intervals[devicename] = 1
if battery is not None and battery <= 33 and distancefromhome > 3:
self._intervals[devicename] = self._intervals[devicename] * 2
def update_device(self, devicename):
"""Update the device_tracker entity."""
from pyicloud.exceptions import PyiCloudNoDevicesException
# An entity will not be created by see() when track=false in
# 'known_devices.yaml', but we need to see() it at least once
entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename))
if entity is None and devicename in self.seen_devices:
return
attrs = {}
kwargs = {}
if self.api is None:
return
try:
for device in self.api.devices:
if str(device) != str(self.devices[devicename]):
continue
status = device.status(DEVICESTATUSSET)
dev_id = status['name'].replace(' ', '', 99)
dev_id = slugify(dev_id)
attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get(
status['deviceStatus'], 'error')
attrs[ATTR_LOWPOWERMODE] = status['lowPowerMode']
attrs[ATTR_BATTERYSTATUS] = status['batteryStatus']
attrs[ATTR_ACCOUNTNAME] = self.accountname
status = device.status(DEVICESTATUSSET)
battery = status.get('batteryLevel', 0) * 100
location = status['location']
if location:
self.determine_interval(
devicename, location['latitude'],
location['longitude'], battery)
interval = self._intervals.get(devicename, 1)
attrs[ATTR_INTERVAL] = interval
accuracy = location['horizontalAccuracy']
kwargs['dev_id'] = dev_id
kwargs['host_name'] = status['name']
kwargs['gps'] = (location['latitude'],
location['longitude'])
kwargs['battery'] = battery
kwargs['gps_accuracy'] = accuracy
kwargs[ATTR_ATTRIBUTES] = attrs
self.see(**kwargs)
self.seen_devices[devicename] = True
except PyiCloudNoDevicesException:
_LOGGER.error('No iCloud Devices found!')
def lost_iphone(self, devicename):
"""Call the lost iphone function if the device is found."""
if self.api is None:
return
self.api.authenticate()
for device in self.api.devices:
if devicename is None or device == self.devices[devicename]:
device.play_sound()
def update_icloud(self, devicename=None):
"""Authenticate against iCloud and scan for devices."""
from pyicloud.exceptions import PyiCloudNoDevicesException
if self.api is None:
return
try:
if devicename is not None:
if devicename in self.devices:
self.devices[devicename].update_icloud()
else:
_LOGGER.error("devicename %s unknown for account %s",
devicename, self._attrs[ATTR_ACCOUNTNAME])
else:
for device in self.devices:
self.devices[device].update_icloud()
except PyiCloudNoDevicesException:
_LOGGER.error('No iCloud Devices found!')
def setinterval(self, interval=None, devicename=None):
"""Set the interval of the given devices."""
devs = [devicename] if devicename else self.devices
for device in devs:
devid = DOMAIN + '.' + device
devicestate = self.hass.states.get(devid)
if interval is not None:
if devicestate is not None:
self._overridestates[device] = active_zone(
self.hass,
float(devicestate.attributes.get('latitude', 0)),
float(devicestate.attributes.get('longitude', 0)))
if self._overridestates[device] is None:
self._overridestates[device] = 'away'
self._intervals[device] = interval
else:
self._overridestates[device] = None
self.update_device(device)

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