Compare commits

...

152 Commits

Author SHA1 Message Date
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
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 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 40094cecae Fix slack targets (#3826) 2016-10-11 21:16:11 -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
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
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
231 changed files with 10502 additions and 3274 deletions
+19 -3
View File
@@ -31,6 +31,9 @@ omit =
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
@@ -95,8 +98,7 @@ omit =
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
homeassistant/components/switch/pilight.py
homeassistant/components/knx.py
homeassistant/components/*/knx.py
@@ -104,16 +106,22 @@ omit =
homeassistant/components/ffmpeg.py
homeassistant/components/*/ffmpeg.py
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.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
@@ -127,6 +135,7 @@ omit =
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/bbox.py
homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bluetooth_le_tracker.py
homeassistant/components/device_tracker/bt_home_hub_5.py
@@ -144,6 +153,7 @@ 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
@@ -197,6 +207,7 @@ 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/nma.py
homeassistant/components/notify/pushbullet.py
@@ -209,6 +220,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,6 +228,8 @@ 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
@@ -235,6 +249,7 @@ 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/hp_ilo.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py
@@ -254,6 +269,7 @@ omit =
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.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
@@ -272,7 +288,6 @@ omit =
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
@@ -281,6 +296,7 @@ omit =
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/mystrom.py
homeassistant/components/switch/neato.py
homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pulseaudio_loopback.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**
+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
+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:
+32 -8
View File
@@ -32,12 +32,15 @@ _CURRENT_SETUP = []
ATTR_COMPONENT = 'component'
ERROR_LOG_FILENAME = 'home-assistant.log'
_PERSISTENT_PLATFORMS = set()
_PERSISTENT_VALIDATION = set()
def setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies."""
if domain in hass.config.components:
_LOGGER.debug('Component %s already set up.', domain)
return True
_ensure_loader_prepared(hass)
@@ -53,6 +56,7 @@ def setup_component(hass: core.HomeAssistant, domain: str,
for component in components:
if not _setup_component(hass, component, config):
_LOGGER.error('Component %s failed to setup', component)
return False
return True
@@ -147,7 +151,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)
log_exception(ex, domain, config, hass)
return None
elif hasattr(component, 'PLATFORM_SCHEMA'):
@@ -157,8 +161,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
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
@@ -171,7 +175,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
p_name)
if platform is None:
return None
continue
# Validate platform specific schema
if hasattr(platform, 'PLATFORM_SCHEMA'):
@@ -179,8 +183,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.Invalid as ex:
log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated)
return None
p_validated, hass)
continue
platforms.append(p_validated)
@@ -209,6 +213,13 @@ 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)
_PERSISTENT_PLATFORMS.add(platform_path)
message = ('Unable to find the following platforms: ' +
', '.join(list(_PERSISTENT_PLATFORMS)) +
'(please check your configuration)')
persistent_notification.create(
hass, message, 'Invalid platforms', 'platform_errors')
return None
# Already loaded
@@ -255,7 +266,7 @@ def from_config_dict(config: Dict[str, Any],
try:
conf_util.process_ha_core_config(hass, core_config)
except vol.Invalid as ex:
log_exception(ex, 'homeassistant', core_config)
log_exception(ex, 'homeassistant', core_config, hass)
return None
conf_util.process_ha_config_upgrade(hass)
@@ -303,6 +314,7 @@ def from_config_dict(config: Dict[str, Any],
hass.loop.run_until_complete(
hass.loop.run_in_executor(None, component_setup)
)
return hass
@@ -343,6 +355,11 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
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)
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
@@ -395,9 +412,16 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
loader.prepare(hass)
def log_exception(ex, domain, config):
def log_exception(ex, domain, config, hass=None):
"""Generate log exception for config validation."""
message = 'Invalid config for [{}]: '.format(domain)
if hass is not None:
_PERSISTENT_VALIDATION.add(domain)
message = ('The following platforms contain invalid configuration: ' +
', '.join(list(_PERSISTENT_VALIDATION)) +
' (please check your configuration)')
persistent_notification.create(
hass, message, 'Invalid config', 'invalid_config')
if 'extra keys not allowed' in ex.error_message:
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
+136
View File
@@ -0,0 +1,136 @@
"""
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 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
import requests
import voluptuous as vol
REQUIREMENTS = ['concord232==0.14']
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232'
DEFAULT_PORT = 5007
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,
})
SCAN_INTERVAL = 1
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup concord232 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):
"""Initalize 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()
@@ -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."""
+105 -5
View File
@@ -7,16 +7,20 @@ https://home-assistant.io/components/alexa/
import copy
import enum
import logging
import uuid
from datetime import datetime
import voluptuous as vol
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 +32,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 +82,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 +99,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.wsgi.register_view(AlexaIntentsView(hass, intents))
hass.wsgi.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):
@@ -235,3 +269,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)
# pylint: disable=too-many-branches
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 self.Response(status=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].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].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].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].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)
+21 -19
View File
@@ -11,6 +11,7 @@ import os
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant import config as conf_util
from homeassistant.const import (
@@ -157,24 +158,24 @@ def setup(hass, config):
descriptions = conf_util.load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
@callback
def trigger_service_handler(service_call):
"""Handle automation triggers."""
for entity in component.extract_from_service(service_call):
for entity in component.async_extract_from_service(service_call):
hass.loop.create_task(entity.async_trigger(
service_call.data.get(ATTR_VARIABLES), True))
@asyncio.coroutine
@callback
def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls."""
method = 'async_{}'.format(service_call.service)
for entity in component.extract_from_service(service_call):
for entity in component.async_extract_from_service(service_call):
hass.loop.create_task(getattr(entity, method)())
@asyncio.coroutine
@callback
def toggle_service_handler(service_call):
"""Handle automation toggle service calls."""
for entity in component.extract_from_service(service_call):
for entity in component.async_extract_from_service(service_call):
if entity.is_on:
hass.loop.create_task(entity.async_turn_off())
else:
@@ -183,8 +184,7 @@ def setup(hass, config):
@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))
@@ -271,7 +271,9 @@ 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())
# It's important that the update is finished before this method
# ends because async_remove depends on it.
yield from self.async_update_ha_state()
@asyncio.coroutine
def async_trigger(self, variables, skip_condition=False):
@@ -280,15 +282,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())
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 +343,11 @@ 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)
hass.loop.create_task(component.async_add_entities(entities))
return len(entities) > 0
@@ -356,10 +357,11 @@ 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)
logbook.async_log_entry(
hass, name, 'has been triggered', DOMAIN, entity_id)
hass.loop.create_task(script_obj.async_run(variables))
return action
+143
View File
@@ -0,0 +1,143 @@
"""
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
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
import requests
import voluptuous as vol
REQUIREMENTS = ['concord232==0.14']
_LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE_ZONES = 'exclude_zones'
CONF_ZONE_TYPES = 'zone_types'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = '5007'
DEFAULT_SSL = False
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,
})
SCAN_INTERVAL = 1
DEFAULT_NAME = "Alarm"
# pylint: disable=too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup 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 frm 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)
@@ -0,0 +1,127 @@
"""
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
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_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)
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,
variable)])
class WelcomeBinarySensor(BinarySensorDevice):
"""Represent a single binary sensor in a Netatmo Welcome device."""
def __init__(self, data, camera_name, home, sensor):
"""Setup for access to the Netatmo camera events."""
self._data = data
self._camera_name = camera_name
self._home = home
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)
elif self._sensor_name == "Someone unknown":
self._state =\
self._data.welcomedata.someoneUnknownSeen(self._home,
self._camera_name)
elif self._sensor_name == "Motion":
self._state =\
self._data.welcomedata.motionDetected(self._home,
self._camera_name)
else:
return None
@@ -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,
@@ -52,7 +51,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,7 +69,7 @@ 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(
@@ -109,8 +108,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))
+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,8 +62,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if not sensors:
_LOGGER.error('No sensors added')
return False
add_devices(sensors)
hass.loop.create_task(async_add_devices(sensors))
return True
@@ -74,21 +75,22 @@ class BinarySensorTemplate(BinarySensorDevice):
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()
self._async_render()
@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))
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 +114,11 @@ class BinarySensorTemplate(BinarySensorDevice):
@asyncio.coroutine
def async_update(self):
"""Get the latest data and update the state."""
"""Update the state from the template."""
self._async_render()
def _async_render(self):
"""Render the state from the template."""
try:
self._state = self._template.async_render().lower() == 'true'
except TemplateError as ex:
+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
+223
View File
@@ -0,0 +1,223 @@
"""
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 logging
import voluptuous as vol
import requests
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-locals
DEFAULT_NAME = 'Synology Camera'
DEFAULT_STREAM_ID = '0'
TIMEOUT = 5
CONF_CAMERA_NAME = 'camera_name'
CONF_STREAM_ID = 'stream_id'
CONF_VALID_CERT = 'valid_cert'
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'
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_VALID_CERT, default=True): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a Synology IP Camera."""
# 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.'}
query_req = requests.get(syno_api_url,
params=query_payload,
verify=config.get(CONF_VALID_CERT),
timeout=TIMEOUT)
query_resp = 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']
# 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 = get_session_id(config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
syno_auth_url,
config.get(CONF_VALID_CERT))
# 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'}
camera_req = requests.get(syno_camera_url,
params=camera_payload,
verify=config.get(CONF_VALID_CERT),
timeout=TIMEOUT,
cookies={'id': session_id})
camera_resp = camera_req.json()
cameras = camera_resp['data']['cameras']
for camera in cameras:
if not config.get(CONF_WHITELIST):
camera_id = camera['id']
snapshot_path = camera['snapshot_path']
add_devices([SynologyCamera(config,
camera_id,
camera['name'],
snapshot_path,
streaming_path,
camera_path,
auth_path)])
def get_session_id(username, password, login_url, valid_cert):
"""Get a session id."""
auth_payload = {'api': AUTH_API,
'method': 'Login',
'version': '2',
'account': username,
'passwd': password,
'session': 'SurveillanceStation',
'format': 'sid'}
auth_req = requests.get(login_url,
params=auth_payload,
verify=valid_cert,
timeout=TIMEOUT)
auth_resp = auth_req.json()
return auth_resp['data']['sid']
# pylint: disable=too-many-instance-attributes
class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera."""
# pylint: disable=too-many-arguments
def __init__(self, config, camera_id, camera_name,
snapshot_path, streaming_path, camera_path, auth_path):
"""Initialize a Synology Surveillance Station camera."""
super().__init__()
self._name = camera_name
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._synology_url = config.get(CONF_URL)
self._api_url = config.get(CONF_URL) + 'webapi/'
self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi'
self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID)
self._valid_cert = config.get(CONF_VALID_CERT)
self._camera_id = camera_id
self._snapshot_path = snapshot_path
self._streaming_path = streaming_path
self._camera_path = camera_path
self._auth_path = auth_path
self._session_id = get_session_id(self._username,
self._password,
self._login_url,
self._valid_cert)
def get_sid(self):
"""Get a session id."""
auth_payload = {'api': AUTH_API,
'method': 'Login',
'version': '2',
'account': self._username,
'passwd': self._password,
'session': 'SurveillanceStation',
'format': 'sid'}
auth_req = requests.get(self._login_url,
params=auth_payload,
verify=self._valid_cert,
timeout=TIMEOUT)
auth_resp = auth_req.json()
self._session_id = auth_resp['data']['sid']
def 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:
response = requests.get(image_url,
params=image_payload,
timeout=TIMEOUT,
verify=self._valid_cert,
cookies={'id': self._session_id})
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
return response.content
def camera_stream(self):
"""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'}
response = requests.get(streaming_url,
payload=streaming_payload,
stream=True,
timeout=TIMEOUT,
cookies={'id': self._session_id})
return response
def mjpeg_steam(self, response):
"""Generate an HTTP MJPEG Stream from the Synology NAS."""
stream = self.camera_stream()
return response(
stream.iter_content(chunk_size=1024),
mimetype=stream.headers['CONTENT_TYPE_HEADER'],
direct_passthrough=True
)
@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
+22 -11
View File
@@ -253,7 +253,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
@@ -368,7 +368,10 @@ class ClimateDevice(Entity):
@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 +401,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 +428,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 +545,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 +567,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 -1
View File
@@ -59,7 +59,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 -6
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
@@ -105,12 +105,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):
+26 -11
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():
@@ -30,14 +44,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
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):
@@ -100,7 +100,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
+32 -34
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
@@ -69,7 +67,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 +75,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 +83,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
@@ -41,7 +41,7 @@ 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
@@ -120,7 +120,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
@@ -217,7 +217,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)
+1 -1
View File
@@ -63,7 +63,7 @@ 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
@@ -47,7 +47,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)
+29 -9
View File
@@ -40,8 +40,19 @@ 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)
@property
def name(self):
@@ -57,18 +68,22 @@ class NestThermostat(ClimateDevice):
return location.capitalize() + '(' + 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.device.has_humidifier or self.device.has_dehumidifier:
# Move these to Thermostat Device and make them global
return {
"humidity": self.device.humidity,
"target_humidity": self.device.target_humidity,
}
else:
# No way to control humidity not show setting
return {}
@property
def current_temperature(self):
@@ -164,7 +179,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.device.has_fan:
# Return whether the fan is on
return STATE_ON if self.device.fan else STATE_AUTO
else:
# No Fan available so disable slider
return None
@property
def fan_list(self):
+178
View File
@@ -0,0 +1,178 @@
"""
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
# pylint: disable=abstract-method
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.0']
ATTR_FAN = 'fan'
@@ -69,7 +69,7 @@ class ProliphixThermostat(ClimateDevice):
}
@property
def unit_of_measurement(self):
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@@ -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
+1 -1
View File
@@ -93,7 +93,7 @@ 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
+1 -1
View File
@@ -209,7 +209,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
+12 -10
View File
@@ -67,31 +67,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)
@@ -6,6 +6,7 @@ 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
@@ -13,6 +14,7 @@ import threading
from typing import Any, Sequence, Callable
import voluptuous as vol
import yaml
from homeassistant.bootstrap import (
prepare_setup_platform, log_exception)
@@ -25,6 +27,7 @@ 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.helpers.event import track_utc_time_change
@@ -66,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',
@@ -114,7 +113,7 @@ def setup(hass: HomeAssistantType, config: ConfigType):
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)
return False
@@ -252,9 +251,18 @@ class DeviceTracker(object):
def setup_group(self):
"""Initialize group for all tracked devices."""
run_coroutine_threadsafe(
self.async_setup_group(), self.hass.loop).result()
@asyncio.coroutine
def async_setup_group(self):
"""Initialize group for all tracked devices.
This method must be run in the event loop.
"""
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):
@@ -413,7 +421,12 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
})
try:
result = []
devices = load_yaml_config_file(path)
try:
devices = 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)
@@ -456,14 +469,15 @@ def update_config(path: str, dev_id: str, device: Device):
"""Add device to YAML configuration file."""
with open(path, 'a') as out:
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))
device = {device.dev_id: {
'name': device.name,
'mac': device.mac,
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide
}}
yaml.dump(device, out, default_flow_style=False)
def get_gravatar_for_email(email: str):
@@ -0,0 +1,82 @@
"""
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
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pybbox==0.0.5-alpha']
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
@@ -79,7 +79,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
@@ -11,13 +11,14 @@ import voluptuous as vol
import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_DEVICES
from homeassistant.components.mqtt import CONF_QOS
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['mqtt']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic},
})
@@ -30,7 +30,7 @@ CONF_EXCLUDE = 'exclude'
REQUIREMENTS = ['python-nmap==0.6.1']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOSTS): cv.string,
vol.Required(CONF_HOSTS): cv.ensure_list,
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
vol.Optional(CONF_EXCLUDE, default=[]):
vol.All(cv.ensure_list, vol.Length(min=1))
@@ -120,7 +120,8 @@ class NmapDeviceScanner(object):
options += ' --exclude {}'.format(','.join(exclude_hosts))
try:
result = scanner.scan(hosts=self.hosts, arguments=options)
result = scanner.scan(hosts=' '.join(self.hosts),
arguments=options)
except PortScannerError:
return False
@@ -23,11 +23,17 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.3.2']
CONF_COMMUNITY = "community"
CONF_AUTHKEY = "authkey"
CONF_PRIVKEY = "privkey"
CONF_BASEOID = "baseoid"
DEFAULT_COMMUNITY = "public"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_COMMUNITY): cv.string,
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
vol.Inclusive(CONF_AUTHKEY, "keys"): cv.string,
vol.Inclusive(CONF_PRIVKEY, "keys"): cv.string,
vol.Required(CONF_BASEOID): cv.string
})
@@ -43,13 +49,24 @@ def get_scanner(hass, config):
class SnmpScanner(object):
"""Queries any SNMP capable Access Point for connected devices."""
# pylint: disable=too-many-instance-attributes
def __init__(self, config):
"""Initialize the scanner."""
from pysnmp.entity.rfc3413.oneliner import cmdgen
from pysnmp.entity import config as cfg
self.snmp = cmdgen.CommandGenerator()
self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161))
self.community = cmdgen.CommunityData(config[CONF_COMMUNITY])
if CONF_AUTHKEY not in config or CONF_PRIVKEY not in config:
self.auth = cmdgen.CommunityData(config[CONF_COMMUNITY])
else:
self.auth = cmdgen.UsmUserData(
config[CONF_COMMUNITY],
config[CONF_AUTHKEY],
config[CONF_PRIVKEY],
authProtocol=cfg.usmHMACSHAAuthProtocol,
privProtocol=cfg.usmAesCfb128Protocol
)
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
self.lock = threading.Lock()
@@ -95,7 +112,7 @@ class SnmpScanner(object):
devices = []
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
self.community, self.host, self.baseoid)
self.auth, self.host, self.baseoid)
if errindication:
_LOGGER.error("SNMPLIB error: %s", errindication)
+2 -2
View File
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.9.0']
REQUIREMENTS = ['python-digitalocean==1.10.0']
_LOGGER = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ CONFIG_SCHEMA = vol.Schema({
# pylint: disable=unused-argument,too-few-public-methods
def setup(hass, config):
"""Setup the Digital Ocean component."""
"""Set up the Digital Ocean component."""
conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN)
+3 -1
View File
@@ -14,15 +14,17 @@ import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover
REQUIREMENTS = ['netdisco==0.7.1']
REQUIREMENTS = ['netdisco==0.7.2']
DOMAIN = 'discovery'
SCAN_INTERVAL = 300 # seconds
SERVICE_NETGEAR = 'netgear_router'
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HASS_IOS_APP = 'hass_ios'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
SERVICE_WEMO: ('wemo', None),
'philips_hue': ('light', 'hue'),
@@ -0,0 +1,95 @@
"""
A component which allows you to send data to Emoncms.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emoncms_history/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import requests
from homeassistant.const import (
CONF_API_KEY, CONF_WHITELIST,
CONF_URL, STATE_UNKNOWN,
STATE_UNAVAILABLE,
CONF_SCAN_INTERVAL)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper
from homeassistant.helpers.event import track_point_in_time
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
DOMAIN = "emoncms_history"
CONF_INPUTNODE = "inputnode"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_URL): cv.string,
vol.Required(CONF_INPUTNODE): cv.positive_int,
vol.Required(CONF_WHITELIST): cv.entity_ids,
vol.Optional(CONF_SCAN_INTERVAL, default=30): cv.positive_int,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup the emoncms_history component."""
conf = config[DOMAIN]
whitelist = conf.get(CONF_WHITELIST)
def send_data(url, apikey, node, payload):
"""Send payload data to emoncms."""
try:
fullurl = "{}/input/post.json".format(url)
req = requests.post(fullurl,
params={"node": node},
data={"apikey": apikey,
"data": payload},
allow_redirects=True,
timeout=5)
except requests.exceptions.RequestException:
_LOGGER.error("Error saving data '%s' to '%s'",
payload, fullurl)
else:
if req.status_code != 200:
_LOGGER.error("Error saving data '%s' to '%s'" +
"(http status code = %d)", payload,
fullurl, req.status_code)
def update_emoncms(time):
"""Send whitelisted entities states reguarly to emoncms."""
payload_dict = {}
for entity_id in whitelist:
state = hass.states.get(entity_id)
if state is None or state.state in (
STATE_UNKNOWN, "", STATE_UNAVAILABLE):
continue
try:
payload_dict[entity_id] = state_helper.state_as_number(
state)
except ValueError:
continue
if len(payload_dict) > 0:
payload = "{%s}" % ",".join("{}:{}".format(key, val)
for key, val in
payload_dict.items())
send_data(conf.get(CONF_URL), conf.get(CONF_API_KEY),
str(conf.get(CONF_INPUTNODE)), payload)
track_point_in_time(hass, update_emoncms, time +
timedelta(seconds=conf.get(
CONF_SCAN_INTERVAL)))
update_emoncms(dt_util.utcnow())
return True
+1 -1
View File
@@ -77,7 +77,7 @@ def setup(hass, yaml_config):
ssl_certificate=None,
ssl_key=None,
cors_origins=[],
approved_ips=[]
trusted_networks=[]
)
server.register_view(DescriptionXmlView(hass, config))
@@ -211,8 +211,14 @@ class IndexView(HomeAssistantView):
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
# auto login if no password was set
no_auth = 'false' if self.hass.config.api.api_password else 'true'
no_auth = 'true'
if self.hass.config.api.api_password:
# require password if set
no_auth = 'false'
if self.hass.wsgi.is_trusted_ip(
self.hass.wsgi.get_real_ip(request)):
# bypass for trusted networks
no_auth = 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = self.templates.get_template('index.html')
+6 -5
View File
@@ -1,16 +1,17 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = {
"core.js": "9b3e5ab4eac7e3b074e0daf3f619a638",
"frontend.html": "5854807d361de26fe93ad474010f19d2",
"core.js": "5ed5e063d66eb252b5b288738c9c2d16",
"frontend.html": "0a4c2c6e86a0a78c2ff3e03842de609d",
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
"panels/ha-panel-dev-service.html": "c7974458ebc33412d95497e99b785e12",
"panels/ha-panel-dev-state.html": "4be627b74e683af14ef779d8203ec674",
"panels/ha-panel-dev-service.html": "d33657c964041d3ebf114e90a922a15e",
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
"panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2",
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
"panels/ha-panel-map.html": "af7d04aff7dd5479c5a0016bc8d4dd7d"
"panels/ha-panel-map.html": "49ab2d6f180f8bdea7cffaa66b8a5d3e"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+164 -86
View File
@@ -4,9 +4,9 @@ Provides functionality to group entities.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/group/
"""
import asyncio
import logging
import os
import threading
import voluptuous as vol
@@ -15,10 +15,13 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
from homeassistant.helpers.entity import Entity, generate_entity_id
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import (
run_callback_threadsafe, run_coroutine_threadsafe)
DOMAIN = 'group'
@@ -87,7 +90,10 @@ def reload(hass):
def expand_entity_ids(hass, entity_ids):
"""Return entity_ids with group entity ids replaced by their members."""
"""Return entity_ids with group entity ids replaced by their members.
Async friendly.
"""
found_ids = []
for entity_id in entity_ids:
@@ -118,7 +124,10 @@ def expand_entity_ids(hass, entity_ids):
def get_entity_ids(hass, entity_id, domain_filter=None):
"""Get members of this group."""
"""Get members of this group.
Async friendly.
"""
group = hass.states.get(entity_id)
if not group or ATTR_ENTITY_ID not in group.attributes:
@@ -139,20 +148,19 @@ def setup(hass, config):
"""Setup all groups found definded in the configuration."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
success = _process_config(hass, config, component)
if not success:
return False
run_coroutine_threadsafe(
_async_process_config(hass, config, component), hass.loop).result()
descriptions = conf_util.load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
def reload_service_handler(service_call):
"""Remove all groups and load new ones from config."""
conf = component.prepare_reload()
conf = yield from component.async_prepare_reload()
if conf is None:
return
_process_config(hass, conf, component)
hass.loop.create_task(_async_process_config(hass, conf, component))
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler,
descriptions[DOMAIN][SERVICE_RELOAD],
@@ -161,48 +169,82 @@ def setup(hass, config):
return True
def _process_config(hass, config, component):
@asyncio.coroutine
def _async_process_config(hass, config, component):
"""Process group configuration."""
groups = []
for object_id, conf in config.get(DOMAIN, {}).items():
name = conf.get(CONF_NAME, object_id)
entity_ids = conf.get(CONF_ENTITIES) or []
icon = conf.get(CONF_ICON)
view = conf.get(CONF_VIEW)
group = Group(hass, name, entity_ids, icon=icon, view=view,
object_id=object_id)
component.add_entities((group,))
# This order is important as groups get a number based on creation
# order.
group = yield from Group.async_create_group(
hass, name, entity_ids, icon=icon, view=view, object_id=object_id)
groups.append(group)
return True
yield from component.async_add_entities(groups)
class Group(Entity):
"""Track a group of entity ids."""
# pylint: disable=too-many-instance-attributes, too-many-arguments
def __init__(self, hass, name, entity_ids=None, user_defined=True,
icon=None, view=False, object_id=None):
"""Initialize a group."""
def __init__(self, hass, name, order=None, user_defined=True, icon=None,
view=False):
"""Initialize a group.
This Object has factory function for creation.
"""
self.hass = hass
self._name = name
self._state = STATE_UNKNOWN
self._order = len(hass.states.entity_ids(DOMAIN))
self._user_defined = user_defined
self._order = order
self._icon = icon
self._view = view
self.entity_id = generate_entity_id(
ENTITY_ID_FORMAT, object_id or name, hass=hass)
self.tracking = []
self.group_on = None
self.group_off = None
self._assumed_state = False
self._lock = threading.Lock()
self._unsub_state_changed = None
self._async_unsub_state_changed = None
@staticmethod
# pylint: disable=too-many-arguments
def create_group(hass, name, entity_ids=None, user_defined=True,
icon=None, view=False, object_id=None):
"""Initialize a group."""
return run_coroutine_threadsafe(
Group.async_create_group(hass, name, entity_ids, user_defined,
icon, view, object_id),
hass.loop).result()
@staticmethod
@asyncio.coroutine
# pylint: disable=too-many-arguments
def async_create_group(hass, name, entity_ids=None, user_defined=True,
icon=None, view=False, object_id=None):
"""Initialize a group.
This method must be run in the event loop.
"""
group = Group(
hass, name,
order=len(hass.states.async_entity_ids(DOMAIN)),
user_defined=user_defined, icon=icon, view=view)
group.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id or name, hass=hass)
# run other async stuff
if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids)
yield from group.async_update_tracked_entity_ids(entity_ids)
else:
self.update_ha_state(True)
yield from group.async_update_ha_state(True)
return group
@property
def should_poll(self):
@@ -249,40 +291,74 @@ class Group(Entity):
def update_tracked_entity_ids(self, entity_ids):
"""Update the member entity IDs."""
self.stop()
run_coroutine_threadsafe(
self.async_update_tracked_entity_ids(entity_ids), self.hass.loop
).result()
@asyncio.coroutine
def async_update_tracked_entity_ids(self, entity_ids):
"""Update the member entity IDs.
This method must be run in the event loop.
"""
yield from self.async_stop()
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
self.group_on, self.group_off = None, None
self.update_ha_state(True)
self.start()
yield from self.async_update_ha_state(True)
self.async_start()
def start(self):
"""Start tracking members."""
self._unsub_state_changed = track_state_change(
self.hass, self.tracking, self._state_changed_listener)
run_callback_threadsafe(self.hass.loop, self.async_start).result()
def async_start(self):
"""Start tracking members.
This method must be run in the event loop.
"""
self._async_unsub_state_changed = async_track_state_change(
self.hass, self.tracking, self._state_changed_listener
)
def stop(self):
"""Unregister the group from Home Assistant."""
self.remove()
run_coroutine_threadsafe(self.async_stop(), self.hass.loop).result()
def update(self):
@asyncio.coroutine
def async_stop(self):
"""Unregister the group from Home Assistant.
This method must be run in the event loop.
"""
yield from self.async_remove()
@asyncio.coroutine
def async_update(self):
"""Query all members and determine current group state."""
self._state = STATE_UNKNOWN
self._update_group_state()
self._async_update_group_state()
def remove(self):
"""Remove group from HASS."""
super().remove()
@asyncio.coroutine
def async_remove(self):
"""Remove group from HASS.
if self._unsub_state_changed:
self._unsub_state_changed()
self._unsub_state_changed = None
This method must be run in the event loop.
"""
yield from super().async_remove()
if self._async_unsub_state_changed:
self._async_unsub_state_changed()
self._async_unsub_state_changed = None
@callback
def _state_changed_listener(self, entity_id, old_state, new_state):
"""Respond to a member state changing."""
self._update_group_state(new_state)
self.update_ha_state()
"""Respond to a member state changing.
This method must be run in the event loop.
"""
self._async_update_group_state(new_state)
self.hass.loop.create_task(self.async_update_ha_state())
@property
def _tracking_states(self):
@@ -297,62 +373,64 @@ class Group(Entity):
return states
def _update_group_state(self, tr_state=None):
@callback
def _async_update_group_state(self, tr_state=None):
"""Update group state.
Optionally you can provide the only state changed since last update
allowing this method to take shortcuts.
This method must be run in the event loop.
"""
# pylint: disable=too-many-branches
# To store current states of group entities. Might not be needed.
with self._lock:
states = None
gr_state = self._state
gr_on = self.group_on
gr_off = self.group_off
states = None
gr_state = self._state
gr_on = self.group_on
gr_off = self.group_off
# We have not determined type of group yet
if gr_on is None:
if tr_state is None:
states = self._tracking_states
# We have not determined type of group yet
if gr_on is None:
if tr_state is None:
states = self._tracking_states
for state in states:
gr_on, gr_off = \
_get_group_on_off(state.state)
if gr_on is not None:
break
else:
gr_on, gr_off = _get_group_on_off(tr_state.state)
for state in states:
gr_on, gr_off = \
_get_group_on_off(state.state)
if gr_on is not None:
break
else:
gr_on, gr_off = _get_group_on_off(tr_state.state)
if gr_on is not None:
self.group_on, self.group_off = gr_on, gr_off
if gr_on is not None:
self.group_on, self.group_off = gr_on, gr_off
# We cannot determine state of the group
if gr_on is None:
return
# We cannot determine state of the group
if gr_on is None:
return
if tr_state is None or ((gr_state == gr_on and
tr_state.state == gr_off) or
tr_state.state not in (gr_on, gr_off)):
if states is None:
states = self._tracking_states
if tr_state is None or ((gr_state == gr_on and
tr_state.state == gr_off) or
tr_state.state not in (gr_on, gr_off)):
if states is None:
states = self._tracking_states
if any(state.state == gr_on for state in states):
self._state = gr_on
else:
self._state = gr_off
if any(state.state == gr_on for state in states):
self._state = gr_on
else:
self._state = gr_off
elif tr_state.state in (gr_on, gr_off):
self._state = tr_state.state
elif tr_state.state in (gr_on, gr_off):
self._state = tr_state.state
if tr_state is None or self._assumed_state and \
not tr_state.attributes.get(ATTR_ASSUMED_STATE):
if states is None:
states = self._tracking_states
if tr_state is None or self._assumed_state and \
not tr_state.attributes.get(ATTR_ASSUMED_STATE):
if states is None:
states = self._tracking_states
self._assumed_state = any(
state.attributes.get(ATTR_ASSUMED_STATE) for state
in states)
self._assumed_state = any(
state.attributes.get(ATTR_ASSUMED_STATE) for state
in states)
elif tr_state.attributes.get(ATTR_ASSUMED_STATE):
self._assumed_state = True
elif tr_state.attributes.get(ATTR_ASSUMED_STATE):
self._assumed_state = True
+128 -25
View File
@@ -7,16 +7,39 @@ https://home-assistant.io/components/history/
from collections import defaultdict
from datetime import timedelta
from itertools import groupby
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, script
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ATTR_HIDDEN
DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http']
SIGNIFICANT_DOMAINS = ('thermostat',)
CONF_EXCLUDE = 'exclude'
CONF_INCLUDE = 'include'
CONF_ENTITIES = 'entities'
CONF_DOMAINS = 'domains'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
CONF_EXCLUDE: vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
}),
CONF_INCLUDE: vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]):
vol.All(cv.ensure_list, [cv.string])
})
}),
}, extra=vol.ALLOW_EXTRA)
SIGNIFICANT_DOMAINS = ('thermostat', 'climate')
IGNORE_DOMAINS = ('zone', 'scene',)
@@ -32,7 +55,8 @@ def last_5_states(entity_id):
).order_by(states.state_id.desc()).limit(5))
def get_significant_states(start_time, end_time=None, entity_id=None):
def get_significant_states(start_time, end_time=None, entity_id=None,
filters=None):
"""
Return states changes during UTC period start_time - end_time.
@@ -40,25 +64,25 @@ def get_significant_states(start_time, end_time=None, entity_id=None):
as well as all states from certain domains (for instance
thermostat so that we get current temperature in our graphs).
"""
entity_ids = (entity_id.lower(), ) if entity_id is not None else None
states = recorder.get_model('States')
query = recorder.query('States').filter(
(states.domain.in_(SIGNIFICANT_DOMAINS) |
(states.last_changed == states.last_updated)) &
((~states.domain.in_(IGNORE_DOMAINS)) &
(states.last_updated > start_time)))
(states.last_updated > start_time))
if filters:
query = filters.apply(query, entity_ids)
if end_time is not None:
query = query.filter(states.last_updated < end_time)
if entity_id is not None:
query = query.filter_by(entity_id=entity_id.lower())
states = (
state for state in recorder.execute(
query.order_by(states.entity_id, states.last_updated))
if _is_significant(state))
if (_is_significant(state) and
not state.attributes.get(ATTR_HIDDEN, False)))
return states_to_json(states, start_time, entity_id)
return states_to_json(states, start_time, entity_id, filters)
def state_changes_during_period(start_time, end_time=None, entity_id=None):
@@ -80,7 +104,7 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None):
return states_to_json(states, start_time, entity_id)
def get_states(utc_point_in_time, entity_ids=None, run=None):
def get_states(utc_point_in_time, entity_ids=None, run=None, filters=None):
"""Return the states at a specific point in time."""
if run is None:
run = recorder.run_information(utc_point_in_time)
@@ -96,12 +120,11 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
func.max(states.state_id).label('max_state_id')
).filter(
(states.created >= run.start) &
(states.created < utc_point_in_time)
)
if entity_ids is not None:
most_recent_state_ids = most_recent_state_ids.filter(
states.entity_id.in_(entity_ids))
(states.created < utc_point_in_time) &
(~states.domain.in_(IGNORE_DOMAINS)))
if filters:
most_recent_state_ids = filters.apply(most_recent_state_ids,
entity_ids)
most_recent_state_ids = most_recent_state_ids.group_by(
states.entity_id).subquery()
@@ -109,10 +132,12 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
query = recorder.query('States').join(most_recent_state_ids, and_(
states.state_id == most_recent_state_ids.c.max_state_id))
return recorder.execute(query)
for state in recorder.execute(query):
if not state.attributes.get(ATTR_HIDDEN, False):
yield state
def states_to_json(states, start_time, entity_id):
def states_to_json(states, start_time, entity_id, filters=None):
"""Convert SQL results into JSON friendly data structure.
This takes our state list and turns it into a JSON friendly data
@@ -127,7 +152,7 @@ def states_to_json(states, start_time, entity_id):
entity_ids = [entity_id] if entity_id is not None else None
# Get the states at the start time
for state in get_states(start_time, entity_ids):
for state in get_states(start_time, entity_ids, filters=filters):
state.last_changed = start_time
state.last_updated = start_time
result[state.entity_id].append(state)
@@ -140,16 +165,25 @@ def states_to_json(states, start_time, entity_id):
def get_state(utc_point_in_time, entity_id, run=None):
"""Return a state at a specific point in time."""
states = get_states(utc_point_in_time, (entity_id,), run)
states = list(get_states(utc_point_in_time, (entity_id,), run))
return states[0] if states else None
# pylint: disable=unused-argument
def setup(hass, config):
"""Setup the history hooks."""
hass.wsgi.register_view(Last5StatesView)
hass.wsgi.register_view(HistoryPeriodView)
filters = Filters()
exclude = config[DOMAIN].get(CONF_EXCLUDE)
if exclude:
filters.excluded_entities = exclude[CONF_ENTITIES]
filters.excluded_domains = exclude[CONF_DOMAINS]
include = config[DOMAIN].get(CONF_INCLUDE)
if include:
filters.included_entities = include[CONF_ENTITIES]
filters.included_domains = include[CONF_DOMAINS]
hass.wsgi.register_view(Last5StatesView(hass))
hass.wsgi.register_view(HistoryPeriodView(hass, filters))
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
return True
@@ -161,6 +195,10 @@ class Last5StatesView(HomeAssistantView):
url = '/api/history/entity/<entity:entity_id>/recent_states'
name = 'api:history:entity-recent-states'
def __init__(self, hass):
"""Initilalize the history last 5 states view."""
super().__init__(hass)
def get(self, request, entity_id):
"""Retrieve last 5 states of entity."""
return self.json(last_5_states(entity_id))
@@ -173,6 +211,11 @@ class HistoryPeriodView(HomeAssistantView):
name = 'api:history:view-period'
extra_urls = ['/api/history/period/<datetime:datetime>']
def __init__(self, hass, filters):
"""Initilalize the history period view."""
super().__init__(hass)
self.filters = filters
def get(self, request, datetime=None):
"""Return history over a period of time."""
one_day = timedelta(days=1)
@@ -185,8 +228,68 @@ class HistoryPeriodView(HomeAssistantView):
end_time = start_time + one_day
entity_id = request.args.get('filter_entity_id')
return self.json(
get_significant_states(start_time, end_time, entity_id).values())
return self.json(get_significant_states(
start_time, end_time, entity_id, self.filters).values())
# pylint: disable=too-few-public-methods
class Filters(object):
"""Container for the configured include and exclude filters."""
def __init__(self):
"""Initialise the include and exclude filters."""
self.excluded_entities = []
self.excluded_domains = []
self.included_entities = []
self.included_domains = []
def apply(self, query, entity_ids=None):
"""Apply the include/exclude filter on domains and entities on query.
Following rules apply:
* only the include section is configured - just query the specified
entities or domains.
* only the exclude section is configured - filter the specified
entities and domains from all the entities in the system.
* if include and exclude is defined - select the entities specified in
the include and filter out the ones from the exclude list.
"""
states = recorder.get_model('States')
# specific entities requested - do not in/exclude anything
if entity_ids is not None:
return query.filter(states.entity_id.in_(entity_ids))
query = query.filter(~states.domain.in_(IGNORE_DOMAINS))
filter_query = None
# filter if only excluded domain is configured
if self.excluded_domains and not self.included_domains:
filter_query = ~states.domain.in_(self.excluded_domains)
if self.included_entities:
filter_query &= states.entity_id.in_(self.included_entities)
# filter if only included domain is configured
elif not self.excluded_domains and self.included_domains:
filter_query = states.domain.in_(self.included_domains)
if self.included_entities:
filter_query |= states.entity_id.in_(self.included_entities)
# filter if included and excluded domain is configured
elif self.excluded_domains and self.included_domains:
filter_query = ~states.domain.in_(self.excluded_domains)
if self.included_entities:
filter_query &= (states.domain.in_(self.included_domains) |
states.entity_id.in_(self.included_entities))
else:
filter_query &= (states.domain.in_(self.included_domains) & ~
states.domain.in_(self.excluded_domains))
# no domain filter just included entities
elif not self.excluded_domains and not self.included_domains and \
self.included_entities:
filter_query = states.entity_id.in_(self.included_entities)
if filter_query is not None:
query = query.filter(filter_query)
# finally apply excluded entities filter if configured
if self.excluded_entities:
query = query.filter(~states.entity_id.in_(self.excluded_entities))
return query
def _is_significant(state):
+30 -11
View File
@@ -11,6 +11,7 @@ import mimetypes
import threading
import re
import ssl
from ipaddress import ip_address, ip_network
import voluptuous as vol
@@ -27,16 +28,16 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components import persistent_notification
DOMAIN = 'http'
REQUIREMENTS = ('cherrypy==8.1.0', 'static3==0.7.0', 'Werkzeug==0.11.11')
REQUIREMENTS = ('cherrypy==8.1.2', 'static3==0.7.0', 'Werkzeug==0.11.11')
CONF_API_PASSWORD = 'api_password'
CONF_APPROVED_IPS = 'approved_ips'
CONF_SERVER_HOST = 'server_host'
CONF_SERVER_PORT = 'server_port'
CONF_DEVELOPMENT = 'development'
CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_KEY = 'ssl_key'
CONF_CORS_ORIGINS = 'cors_allowed_origins'
CONF_TRUSTED_NETWORKS = 'trusted_networks'
DATA_API_PASSWORD = 'api_password'
NOTIFICATION_ID_LOGIN = 'http-login'
@@ -76,7 +77,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_APPROVED_IPS): vol.All(cv.ensure_list, [cv.string])
vol.Optional(CONF_TRUSTED_NETWORKS):
vol.All(cv.ensure_list, [ip_network])
}),
}, extra=vol.ALLOW_EXTRA)
@@ -113,7 +115,9 @@ def setup(hass, config):
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
approved_ips = conf.get(CONF_APPROVED_IPS, [])
trusted_networks = [
ip_network(trusted_network)
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
server = HomeAssistantWSGI(
hass,
@@ -124,7 +128,7 @@ def setup(hass, config):
ssl_certificate=ssl_certificate,
ssl_key=ssl_key,
cors_origins=cors_origins,
approved_ips=approved_ips
trusted_networks=trusted_networks
)
def start_wsgi_server(event):
@@ -257,7 +261,7 @@ class HomeAssistantWSGI(object):
def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins,
approved_ips):
trusted_networks):
"""Initilalize the WSGI Home Assistant server."""
from werkzeug.wrappers import Response
@@ -276,7 +280,7 @@ class HomeAssistantWSGI(object):
self.server_host = server_host
self.server_port = server_port
self.cors_origins = cors_origins
self.approved_ips = approved_ips
self.trusted_networks = trusted_networks
self.event_forwarder = None
self.server = None
@@ -431,6 +435,19 @@ class HomeAssistantWSGI(object):
environ['PATH_INFO'] = '{}.{}'.format(*fingerprinted.groups())
return app(environ, start_response)
@staticmethod
def get_real_ip(request):
"""Return the clients correct ip address, even in proxied setups."""
if request.access_route:
return request.access_route[-1]
else:
return request.remote_addr
def is_trusted_ip(self, remote_addr):
"""Match an ip address against trusted CIDR networks."""
return any(ip_address(remote_addr) in trusted_network
for trusted_network in self.hass.wsgi.trusted_networks)
class HomeAssistantView(object):
"""Base view for all views."""
@@ -471,13 +488,15 @@ class HomeAssistantView(object):
except AttributeError:
raise MethodNotAllowed
remote_addr = HomeAssistantWSGI.get_real_ip(request)
# Auth code verbose on purpose
authenticated = False
if self.hass.wsgi.api_password is None:
authenticated = True
elif request.remote_addr in self.hass.wsgi.approved_ips:
elif self.hass.wsgi.is_trusted_ip(remote_addr):
authenticated = True
elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
@@ -491,17 +510,17 @@ class HomeAssistantView(object):
if self.requires_auth and not authenticated:
_LOGGER.warning('Login attempt or request with an invalid '
'password from %s', request.remote_addr)
'password from %s', remote_addr)
persistent_notification.create(
self.hass,
'Invalid password used from {}'.format(request.remote_addr),
'Invalid password used from {}'.format(remote_addr),
'Login attempt failed', NOTIFICATION_ID_LOGIN)
raise Unauthorized()
request.authenticated = authenticated
_LOGGER.info('Serving %s to %s (auth: %s)',
request.path, request.remote_addr, authenticated)
request.path, remote_addr, authenticated)
result = handler(request, **values)
+2 -2
View File
@@ -33,8 +33,8 @@ TIMEOUT = 5
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
vol.Optional(CONF_BLACKLIST, default=[]):
vol.All(cv.ensure_list, [cv.entity_id]),
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
+57 -2
View File
@@ -31,6 +31,18 @@ SERVICE_SELECT_OPTION_SCHEMA = vol.Schema({
vol.Required(ATTR_OPTION): cv.string,
})
SERVICE_SELECT_NEXT = 'select_next'
SERVICE_SELECT_NEXT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
SERVICE_SELECT_PREVIOUS = 'select_previous'
SERVICE_SELECT_PREVIOUS_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
def _cv_input_select(cfg):
"""Config validation helper for input select (Voluptuous)."""
@@ -53,13 +65,27 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: {
def select_option(hass, entity_id, option):
"""Set input_select to False."""
"""Set value of input_select."""
hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, {
ATTR_ENTITY_ID: entity_id,
ATTR_OPTION: option,
})
def select_next(hass, entity_id):
"""Set next value of input_select."""
hass.services.call(DOMAIN, SERVICE_SELECT_NEXT, {
ATTR_ENTITY_ID: entity_id,
})
def select_previous(hass, entity_id):
"""Set previous value of input_select."""
hass.services.call(DOMAIN, SERVICE_SELECT_PREVIOUS, {
ATTR_ENTITY_ID: entity_id,
})
def setup(hass, config):
"""Setup input select."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
@@ -77,7 +103,7 @@ def setup(hass, config):
return False
def select_option_service(call):
"""Handle a calls to the input select services."""
"""Handle a calls to the input select option service."""
target_inputs = component.extract_from_service(call)
for input_select in target_inputs:
@@ -87,6 +113,28 @@ def setup(hass, config):
select_option_service,
schema=SERVICE_SELECT_OPTION_SCHEMA)
def select_next_service(call):
"""Handle a calls to the input select next service."""
target_inputs = component.extract_from_service(call)
for input_select in target_inputs:
input_select.offset_index(1)
hass.services.register(DOMAIN, SERVICE_SELECT_NEXT,
select_next_service,
schema=SERVICE_SELECT_NEXT_SCHEMA)
def select_previous_service(call):
"""Handle a calls to the input select previous service."""
target_inputs = component.extract_from_service(call)
for input_select in target_inputs:
input_select.offset_index(-1)
hass.services.register(DOMAIN, SERVICE_SELECT_PREVIOUS,
select_previous_service,
schema=SERVICE_SELECT_PREVIOUS_SCHEMA)
component.add_entities(entities)
return True
@@ -139,3 +187,10 @@ class InputSelect(Entity):
return
self._current_option = option
self.update_ha_state()
def offset_index(self, offset):
"""Offset current index."""
current_index = self._options.index(self._current_option)
new_index = (current_index + offset) % len(self._options)
self._current_option = self._options[new_index]
self.update_ha_state()
+323
View File
@@ -0,0 +1,323 @@
"""
Native Home Assistant iOS app component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/ios/
"""
import os
import json
import logging
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.helpers import config_validation as cv
import homeassistant.loader as loader
from homeassistant.helpers import discovery
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
HTTP_BAD_REQUEST)
from homeassistant.components.notify import DOMAIN as NotifyDomain
_LOGGER = logging.getLogger(__name__)
DOMAIN = "ios"
DEPENDENCIES = ["http"]
CONF_PUSH = "push"
CONF_PUSH_CATEGORIES = "categories"
CONF_PUSH_CATEGORIES_NAME = "name"
CONF_PUSH_CATEGORIES_IDENTIFIER = "identifier"
CONF_PUSH_CATEGORIES_ACTIONS = "actions"
CONF_PUSH_ACTIONS_IDENTIFIER = "identifier"
CONF_PUSH_ACTIONS_TITLE = "title"
CONF_PUSH_ACTIONS_ACTIVATION_MODE = "activationMode"
CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED = "authenticationRequired"
CONF_PUSH_ACTIONS_DESTRUCTIVE = "destructive"
CONF_PUSH_ACTIONS_BEHAVIOR = "behavior"
CONF_PUSH_ACTIONS_CONTEXT = "context"
CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE = "textInputButtonTitle"
CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER = "textInputPlaceholder"
ATTR_FOREGROUND = "foreground"
ATTR_BACKGROUND = "background"
ACTIVATION_MODES = [ATTR_FOREGROUND, ATTR_BACKGROUND]
ATTR_DEFAULT_BEHAVIOR = "default"
ATTR_TEXT_INPUT_BEHAVIOR = "textInput"
BEHAVIORS = [ATTR_DEFAULT_BEHAVIOR, ATTR_TEXT_INPUT_BEHAVIOR]
ATTR_DEVICE = "device"
ATTR_PUSH_TOKEN = "pushToken"
ATTR_APP = "app"
ATTR_PERMISSIONS = "permissions"
ATTR_PUSH_ID = "pushId"
ATTR_DEVICE_ID = "deviceId"
ATTR_PUSH_SOUNDS = "pushSounds"
ATTR_BATTERY = "battery"
ATTR_DEVICE_NAME = "name"
ATTR_DEVICE_LOCALIZED_MODEL = "localizedModel"
ATTR_DEVICE_MODEL = "model"
ATTR_DEVICE_PERMANENT_ID = "permanentID"
ATTR_DEVICE_SYSTEM_VERSION = "systemVersion"
ATTR_DEVICE_TYPE = "type"
ATTR_DEVICE_SYSTEM_NAME = "systemName"
ATTR_APP_BUNDLE_IDENTIFER = "bundleIdentifer"
ATTR_APP_BUILD_NUMBER = "buildNumber"
ATTR_APP_VERSION_NUMBER = "versionNumber"
ATTR_LOCATION_PERMISSION = "location"
ATTR_NOTIFICATIONS_PERMISSION = "notifications"
PERMISSIONS = [ATTR_LOCATION_PERMISSION, ATTR_NOTIFICATIONS_PERMISSION]
ATTR_BATTERY_STATE = "state"
ATTR_BATTERY_LEVEL = "level"
ATTR_BATTERY_STATE_UNPLUGGED = "Unplugged"
ATTR_BATTERY_STATE_CHARGING = "Charging"
ATTR_BATTERY_STATE_FULL = "Full"
ATTR_BATTERY_STATE_UNKNOWN = "Unknown"
BATTERY_STATES = [ATTR_BATTERY_STATE_UNPLUGGED, ATTR_BATTERY_STATE_CHARGING,
ATTR_BATTERY_STATE_FULL, ATTR_BATTERY_STATE_UNKNOWN]
ATTR_DEVICES = "devices"
ACTION_SCHEMA = vol.Schema({
vol.Required(CONF_PUSH_ACTIONS_IDENTIFIER): vol.Upper,
vol.Required(CONF_PUSH_ACTIONS_TITLE): cv.string,
vol.Optional(CONF_PUSH_ACTIONS_ACTIVATION_MODE,
default=ATTR_BACKGROUND): vol.In(ACTIVATION_MODES),
vol.Optional(CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED,
default=False): cv.boolean,
vol.Optional(CONF_PUSH_ACTIONS_DESTRUCTIVE,
default=False): cv.boolean,
vol.Optional(CONF_PUSH_ACTIONS_BEHAVIOR,
default=ATTR_DEFAULT_BEHAVIOR): vol.In(BEHAVIORS),
vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE): cv.string,
vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER): cv.string,
}, extra=vol.ALLOW_EXTRA)
ACTION_SCHEMA_LIST = vol.All(cv.ensure_list, [ACTION_SCHEMA])
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
CONF_PUSH: {
CONF_PUSH_CATEGORIES: vol.All(cv.ensure_list, [{
vol.Required(CONF_PUSH_CATEGORIES_NAME): cv.string,
vol.Required(CONF_PUSH_CATEGORIES_IDENTIFIER): vol.Upper,
vol.Required(CONF_PUSH_CATEGORIES_ACTIONS): ACTION_SCHEMA_LIST
}])
}
}
}, extra=vol.ALLOW_EXTRA)
IDENTIFY_DEVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_DEVICE_NAME): cv.string,
vol.Required(ATTR_DEVICE_LOCALIZED_MODEL): cv.string,
vol.Required(ATTR_DEVICE_MODEL): cv.string,
vol.Required(ATTR_DEVICE_PERMANENT_ID): cv.string,
vol.Required(ATTR_DEVICE_SYSTEM_VERSION): cv.string,
vol.Required(ATTR_DEVICE_TYPE): cv.string,
vol.Required(ATTR_DEVICE_SYSTEM_NAME): cv.string,
}, extra=vol.ALLOW_EXTRA)
IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA)
IDENTIFY_APP_SCHEMA = vol.Schema({
vol.Required(ATTR_APP_BUNDLE_IDENTIFER): cv.string,
vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int,
vol.Required(ATTR_APP_VERSION_NUMBER): cv.positive_int
}, extra=vol.ALLOW_EXTRA)
IDENTIFY_APP_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_APP_SCHEMA)
IDENTIFY_BATTERY_SCHEMA = vol.Schema({
vol.Required(ATTR_BATTERY_LEVEL): cv.positive_int,
vol.Required(ATTR_BATTERY_STATE): vol.In(BATTERY_STATES)
}, extra=vol.ALLOW_EXTRA)
IDENTIFY_BATTERY_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_BATTERY_SCHEMA)
IDENTIFY_SCHEMA = vol.Schema({
vol.Required(ATTR_DEVICE): IDENTIFY_DEVICE_SCHEMA_CONTAINER,
vol.Required(ATTR_BATTERY): IDENTIFY_BATTERY_SCHEMA_CONTAINER,
vol.Required(ATTR_PUSH_TOKEN): cv.string,
vol.Required(ATTR_APP): IDENTIFY_APP_SCHEMA_CONTAINER,
vol.Required(ATTR_PERMISSIONS): vol.All(cv.ensure_list,
[vol.In(PERMISSIONS)]),
vol.Required(ATTR_PUSH_ID): cv.string,
vol.Required(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_PUSH_SOUNDS): list
}, extra=vol.ALLOW_EXTRA)
CONFIGURATION_FILE = "ios.conf"
CONFIG_FILE = {ATTR_DEVICES: {}}
CONFIG_FILE_PATH = ""
def _load_config(filename):
"""Load configuration."""
if not os.path.isfile(filename):
return {}
try:
with open(filename, "r") as fdesc:
inp = fdesc.read()
# In case empty file
if not inp:
return {}
return json.loads(inp)
except (IOError, ValueError) as error:
_LOGGER.error("Reading config file %s failed: %s", filename, error)
return None
def _save_config(filename, config):
"""Save configuration."""
try:
with open(filename, "w") as fdesc:
fdesc.write(json.dumps(config))
except (IOError, TypeError) as error:
_LOGGER.error("Saving config file failed: %s", error)
return False
return True
def devices_with_push():
"""Return a dictionary of push enabled targets."""
targets = {}
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
if device.get(ATTR_PUSH_ID) is not None:
targets[device_name] = device.get(ATTR_PUSH_ID)
return targets
def enabled_push_ids():
"""Return a list of push enabled target push IDs."""
push_ids = list()
# pylint: disable=unused-variable
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
if device.get(ATTR_PUSH_ID) is not None:
push_ids.append(device.get(ATTR_PUSH_ID))
return push_ids
def devices():
"""Return a dictionary of all identified devices."""
return CONFIG_FILE[ATTR_DEVICES]
def device_name_for_push_id(push_id):
"""Return the device name for the push ID."""
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
if device.get(ATTR_PUSH_ID) is push_id:
return device_name
return None
def setup(hass, config):
"""Setup the iOS component."""
# pylint: disable=global-statement, import-error
global CONFIG_FILE
global CONFIG_FILE_PATH
CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE)
CONFIG_FILE = _load_config(CONFIG_FILE_PATH)
if CONFIG_FILE == {}:
CONFIG_FILE[ATTR_DEVICES] = {}
device_tracker = loader.get_component("device_tracker")
if device_tracker.DOMAIN not in hass.config.components:
device_tracker.setup(hass, {})
# Need this to enable requirements checking in the app.
hass.config.components.append(device_tracker.DOMAIN)
if "notify.ios" not in hass.config.components:
notify = loader.get_component("notify.ios")
notify.get_service(hass, {})
# Need this to enable requirements checking in the app.
if NotifyDomain not in hass.config.components:
hass.config.components.append(NotifyDomain)
zeroconf = loader.get_component("zeroconf")
if zeroconf.DOMAIN not in hass.config.components:
zeroconf.setup(hass, config)
# Need this to enable requirements checking in the app.
hass.config.components.append(zeroconf.DOMAIN)
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
hass.wsgi.register_view(iOSIdentifyDeviceView(hass))
if config.get(DOMAIN) is not None:
app_config = config[DOMAIN]
if app_config.get(CONF_PUSH) is not None:
push_config = app_config[CONF_PUSH]
hass.wsgi.register_view(iOSPushConfigView(hass, push_config))
return True
# pylint: disable=invalid-name
class iOSPushConfigView(HomeAssistantView):
"""A view that provides the push categories configuration."""
url = "/api/ios/push"
name = "api:ios:push"
def __init__(self, hass, push_config):
"""Init the view."""
super().__init__(hass)
self.push_config = push_config
def get(self, request):
"""Handle the GET request for the push configuration."""
return self.json(self.push_config)
class iOSIdentifyDeviceView(HomeAssistantView):
"""A view that accepts device identification requests."""
url = "/api/ios/identify"
name = "api:ios:identify"
def __init__(self, hass):
"""Init the view."""
super().__init__(hass)
def post(self, request):
"""Handle the POST request for device identification."""
try:
data = IDENTIFY_SCHEMA(request.json)
except vol.Invalid as ex:
return self.json_message(humanize_error(request.json, ex),
HTTP_BAD_REQUEST)
name = data.get(ATTR_DEVICE_ID)
CONFIG_FILE[ATTR_DEVICES][name] = data
if not _save_config(CONFIG_FILE_PATH, CONFIG_FILE):
return self.json_message("Error saving device.",
HTTP_INTERNAL_SERVER_ERROR)
return self.json({"status": "registered"})
+1 -1
View File
@@ -18,7 +18,7 @@ from homeassistant.components.light import (
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.7.zip'
'#flux_led==0.7']
'#flux_led==0.8']
_LOGGER = logging.getLogger(__name__)
+52 -22
View File
@@ -24,6 +24,26 @@ AEOTEC = 0x86
AEOTEC_ZW098_LED_BULB = 0x62
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
LINEAR = 0x14f
LINEAR_WD500Z_DIMMER = 0x3034
LINEAR_WD500Z_DIMMER_LIGHT = (LINEAR, LINEAR_WD500Z_DIMMER)
GE = 0x63
GE_12724_DIMMER = 0x3031
GE_12724_DIMMER_LIGHT = (GE, GE_12724_DIMMER)
DRAGONTECH = 0x184
DRAGONTECH_PD100_DIMMER = 0x3032
DRAGONTECH_PD100_DIMMER_LIGHT = (DRAGONTECH, DRAGONTECH_PD100_DIMMER)
ACT = 0x01
ACT_ZDP100_DIMMER = 0x3030
ACT_ZDP100_DIMMER_LIGHT = (ACT, ACT_ZDP100_DIMMER)
HOMESEER = 0x0c
HOMESEER_WD100_DIMMER = 0x3034
HOMESEER_WD100_DIMMER_LIGHT = (HOMESEER, HOMESEER_WD100_DIMMER)
COLOR_CHANNEL_WARM_WHITE = 0x01
COLOR_CHANNEL_COLD_WHITE = 0x02
COLOR_CHANNEL_RED = 0x04
@@ -31,9 +51,15 @@ COLOR_CHANNEL_GREEN = 0x08
COLOR_CHANNEL_BLUE = 0x10
WORKAROUND_ZW098 = 'zw098'
WORKAROUND_DELAY = 'alt_delay'
DEVICE_MAPPINGS = {
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098,
LINEAR_WD500Z_DIMMER_LIGHT: WORKAROUND_DELAY,
GE_12724_DIMMER_LIGHT: WORKAROUND_DELAY,
DRAGONTECH_PD100_DIMMER_LIGHT: WORKAROUND_DELAY,
ACT_ZDP100_DIMMER_LIGHT: WORKAROUND_DELAY,
HOMESEER_WD100_DIMMER_LIGHT: WORKAROUND_DELAY,
}
# Generate midpoint color temperatures for bulbs that have limited
@@ -94,6 +120,24 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._brightness = None
self._state = None
self._alt_delay = None
self._zw098 = None
# Enable appropriate workaround flags for our device
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
self._zw098 = 1
elif DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_DELAY:
_LOGGER.debug("Dimmer delay workaround enabled for node:"
" %s", value.parent_id)
self._alt_delay = 1
self.update_properties()
# Used for value change event handling
@@ -125,7 +169,10 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
if self._timer is not None and self._timer.isAlive():
self._timer.cancel()
self._timer = Timer(2, _refresh_value)
if self._alt_delay:
self._timer = Timer(5, _refresh_value)
else:
self._timer = Timer(2, _refresh_value)
self._timer.start()
self.update_ha_state()
@@ -180,19 +227,13 @@ class ZwaveColorLight(ZwaveDimmer):
self._color_channels = None
self._rgb = None
self._ct = None
self._zw098 = None
# Here we attempt to find a zwave color value with the same instance
# id as the dimmer value. Currently zwave nodes that change colors
# only include one dimmer and one color command, but this will
# hopefully provide some forward compatibility for new devices that
# have multiple color changing elements.
# Currently zwave nodes only exist with one color element per node.
for value_color in value.node.get_rgbbulbs().values():
if value.instance == value_color.instance:
self._value_color = value_color
self._value_color = value_color
if self._value_color is None:
raise ValueError("No matching color command found.")
raise ValueError("No color command found.")
for value_color_channels in value.node.get_values(
class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR,
@@ -202,17 +243,6 @@ class ZwaveColorLight(ZwaveDimmer):
if self._value_color_channels is None:
raise ValueError("Color Channels not found.")
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
self._zw098 = 1
super().__init__(value)
def update_properties(self):
+1 -1
View File
@@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
REQUIREMENTS = ['python-lirc==1.2.1']
REQUIREMENTS = ['python-lirc==1.2.3']
_LOGGER = logging.getLogger(__name__)
+49 -6
View File
@@ -29,15 +29,22 @@ DEPENDENCIES = ['recorder', 'frontend']
_LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE = 'exclude'
CONF_INCLUDE = 'include'
CONF_ENTITIES = 'entities'
CONF_DOMAINS = 'domains'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
CONF_EXCLUDE: vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.ensure_list,
vol.Optional(CONF_DOMAINS, default=[]): cv.ensure_list
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list,
[cv.string])
}),
CONF_INCLUDE: vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list,
[cv.string])
})
}),
}, extra=vol.ALLOW_EXTRA)
@@ -250,7 +257,7 @@ def humanify(events):
event.time_fired, "Home Assistant", action,
domain=HA_DOMAIN)
elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY:
elif event.event_type == EVENT_LOGBOOK_ENTRY:
domain = event.data.get(ATTR_DOMAIN)
entity_id = event.data.get(ATTR_ENTITY_ID)
if domain is None and entity_id is not None:
@@ -267,15 +274,24 @@ def humanify(events):
def _exclude_events(events, config):
"""Get lists of excluded entities and platforms."""
# pylint: disable=too-many-branches
excluded_entities = []
excluded_domains = []
included_entities = []
included_domains = []
exclude = config[DOMAIN].get(CONF_EXCLUDE)
if exclude:
excluded_entities = exclude[CONF_ENTITIES]
excluded_domains = exclude[CONF_DOMAINS]
include = config[DOMAIN].get(CONF_INCLUDE)
if include:
included_entities = include[CONF_ENTITIES]
included_domains = include[CONF_DOMAINS]
filtered_events = []
for event in events:
domain, entity_id = None, None
if event.event_type == EVENT_STATE_CHANGED:
to_state = State.from_dict(event.data.get('new_state'))
# Do not report on new entities
@@ -288,11 +304,38 @@ def _exclude_events(events, config):
continue
domain = to_state.domain
# check if logbook entry is excluded for this domain
if domain in excluded_domains:
entity_id = to_state.entity_id
elif event.event_type == EVENT_LOGBOOK_ENTRY:
domain = event.data.get(ATTR_DOMAIN)
entity_id = event.data.get(ATTR_ENTITY_ID)
if domain or entity_id:
# filter if only excluded is configured for this domain
if excluded_domains and domain in excluded_domains and \
not included_domains:
if (included_entities and entity_id not in included_entities) \
or not included_entities:
continue
# filter if only included is configured for this domain
elif not excluded_domains and included_domains and \
domain not in included_domains:
if (included_entities and entity_id not in included_entities) \
or not included_entities:
continue
# filter if included and excluded is configured for this domain
elif excluded_domains and included_domains and \
(domain not in included_domains or
domain in excluded_domains):
if (included_entities and entity_id not in included_entities) \
or not included_entities or domain in excluded_domains:
continue
# filter if only included is configured for this entity
elif not excluded_domains and not included_domains and \
included_entities and entity_id not in included_entities:
continue
# check if logbook entry is excluded for this entity
if to_state.entity_id in excluded_entities:
if entity_id in excluded_entities:
continue
filtered_events.append(event)
return filtered_events
@@ -236,6 +236,7 @@ class BraviaTVDevice(MediaPlayerDevice):
if power_status == 'active':
self._state = STATE_ON
playing_info = self._braviarc.get_playing_info()
self._reset_playing_info()
if playing_info is None or len(playing_info) == 0:
self._channel_name = 'App'
else:
@@ -255,6 +256,16 @@ class BraviaTVDevice(MediaPlayerDevice):
_LOGGER.error(exception_instance)
self._state = STATE_OFF
def _reset_playing_info(self):
self._program_name = None
self._channel_name = None
self._program_media_type = None
self._channel_number = None
self._source = None
self._content_uri = None
self._duration = None
self._start_date_time = None
def _refresh_volume(self):
"""Refresh volume information."""
volume_info = self._braviarc.get_volume_info()
+15 -9
View File
@@ -19,7 +19,7 @@ from homeassistant.const import (
STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pychromecast==0.7.4']
REQUIREMENTS = ['pychromecast==0.7.6']
_LOGGER = logging.getLogger(__name__)
@@ -68,12 +68,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
casts = []
# get_chromecasts() returns Chromecast objects
# with the correct friendly name for grouped devices
all_chromecasts = pychromecast.get_chromecasts()
for host in hosts:
try:
casts.append(CastDevice(*host))
KNOWN_HOSTS.append(host)
except pychromecast.ChromecastConnectionError:
pass
found = [device for device in all_chromecasts
if (device.host, device.port) == host]
if found:
try:
casts.append(CastDevice(found[0]))
KNOWN_HOSTS.append(host)
except pychromecast.ChromecastConnectionError:
pass
add_devices(casts)
@@ -83,10 +90,9 @@ class CastDevice(MediaPlayerDevice):
# pylint: disable=abstract-method
# pylint: disable=too-many-public-methods
def __init__(self, host, port):
def __init__(self, chromecast):
"""Initialize the Cast device."""
import pychromecast
self.cast = pychromecast.Chromecast(host, port)
self.cast = chromecast
self.cast.socket_client.receiver_controller.register_status_listener(
self)
@@ -13,12 +13,15 @@ from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_NAME)
CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_NAME, CONF_PORT,
CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Pioneer AVR'
DEFAULT_PORT = 23 # telnet default. Some Pioneer AVRs use 8102
DEFAULT_TIMEOUT = None
SUPPORT_PIONEER = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
@@ -29,12 +32,17 @@ MAX_SOURCE_NUMBERS = 60
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.socket_timeout,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Pioneer platform."""
pioneer = PioneerDevice(config.get(CONF_NAME), config.get(CONF_HOST))
pioneer = PioneerDevice(config.get(CONF_NAME),
config.get(CONF_HOST),
config.get(CONF_PORT),
config.get(CONF_TIMEOUT))
if pioneer.update():
add_devices([pioneer])
@@ -48,10 +56,12 @@ class PioneerDevice(MediaPlayerDevice):
# pylint: disable=too-many-public-methods, abstract-method
# pylint: disable=too-many-instance-attributes
def __init__(self, name, host):
def __init__(self, name, host, port, timeout):
"""Initialize the Pioneer device."""
self._name = name
self._host = host
self._port = port
self._timeout = timeout
self._pwstate = 'PWR1'
self._volume = 0
self._muted = False
@@ -62,7 +72,11 @@ class PioneerDevice(MediaPlayerDevice):
@classmethod
def telnet_request(cls, telnet, command, expected_prefix):
"""Execute `command` and return the response."""
telnet.write(command.encode("ASCII") + b"\r")
try:
telnet.write(command.encode("ASCII") + b"\r")
except telnetlib.socket.timeout:
_LOGGER.debug("Pioneer command %s timed out", command)
return None
# The receiver will randomly send state change updates, make sure
# we get the response we are looking for
@@ -76,19 +90,32 @@ class PioneerDevice(MediaPlayerDevice):
def telnet_command(self, command):
"""Establish a telnet connection and sends `command`."""
telnet = telnetlib.Telnet(self._host)
telnet.write(command.encode("ASCII") + b"\r")
telnet.read_very_eager() # skip response
telnet.close()
try:
try:
telnet = telnetlib.Telnet(self._host,
self._port,
self._timeout)
except ConnectionRefusedError:
_LOGGER.debug("Pioneer %s refused connection", self._name)
return
telnet.write(command.encode("ASCII") + b"\r")
telnet.read_very_eager() # skip response
telnet.close()
except telnetlib.socket.timeout:
_LOGGER.debug(
"Pioneer %s command %s timed out", self._name, command)
def update(self):
"""Get the latest details from the device."""
try:
telnet = telnetlib.Telnet(self._host)
telnet = telnetlib.Telnet(self._host, self._port, self._timeout)
except ConnectionRefusedError:
_LOGGER.debug("Pioneer %s refused connection", self._name)
return False
self._pwstate = self.telnet_request(telnet, "?P", "PWR")
pwstate = self.telnet_request(telnet, "?P", "PWR")
if pwstate:
self._pwstate = pwstate
volume_str = self.telnet_request(telnet, "?V", "VOL")
self._volume = int(volume_str[3:]) / MAX_VOLUME if volume_str else None
@@ -6,23 +6,42 @@ https://home-assistant.io/components/media_player.russound_rnet/
"""
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON)
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/laf/russound/archive/0.1.6.zip'
'#russound==0.1.6']
ZONES = 'zones'
SOURCES = 'sources'
_LOGGER = logging.getLogger(__name__)
CONF_ZONES = 'zones'
CONF_SOURCES = 'sources'
SUPPORT_RUSSOUND = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
_LOGGER = logging.getLogger(__name__)
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
SOURCE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}),
vol.Required(CONF_SOURCES): vol.All(cv.ensure_list, [SOURCE_SCHEMA]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -32,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
keypad = config.get('keypad', '70')
if host is None or port is None:
_LOGGER.error('Invalid config. Expected %s and %s',
_LOGGER.error("Invalid config. Expected %s and %s",
CONF_HOST, CONF_PORT)
return False
@@ -42,13 +61,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
russ.connect(keypad)
sources = []
for source in config[SOURCES]:
for source in config[CONF_SOURCES]:
sources.append(source['name'])
if russ.is_connected():
for zone_id, extra in config[ZONES].items():
add_devices([RussoundRNETDevice(hass, russ, sources, zone_id,
extra)])
for zone_id, extra in config[CONF_ZONES].items():
add_devices([RussoundRNETDevice(
hass, russ, sources, zone_id, extra)])
else:
_LOGGER.error('Not connected to %s:%s', host, port)
+11 -1
View File
@@ -21,7 +21,7 @@ from homeassistant.const import (
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['SoCo==0.11.1']
REQUIREMENTS = ['SoCo==0.12']
_LOGGER = logging.getLogger(__name__)
@@ -62,6 +62,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info:
player = soco.SoCo(discovery_info)
# if device allready exists by config
if player.uid in DEVICES:
return True
if player.is_visible:
device = SonosDevice(hass, player)
add_devices([device])
@@ -212,6 +217,11 @@ class SonosDevice(MediaPlayerDevice):
"""Update state, called by track_utc_time_change."""
self.update_ha_state(True)
@property
def unique_id(self):
"""Return an unique ID."""
return self._player.uid
@property
def name(self):
"""Return the name of the device."""
@@ -40,6 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the squeezebox platform."""
import socket
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
@@ -50,11 +52,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
# Only add a media server once
if host in KNOWN_DEVICES:
# Get IP of host, to prevent duplication of same host (different DNS names)
try:
ipaddr = socket.gethostbyname(host)
except (OSError) as error:
_LOGGER.error("Could not communicate with %s:%d: %s",
host, port, error)
return False
KNOWN_DEVICES.append(host)
# Combine it with port to allow multiple servers at the same host
key = "{}:{}".format(ipaddr, port)
# Only add a media server once
if key in KNOWN_DEVICES:
return False
KNOWN_DEVICES.append(key)
_LOGGER.debug("Creating LMS object for %s", key)
lms = LogitechMediaServer(host, port, username, password)
if not lms.init_success:
@@ -97,56 +111,64 @@ class LogitechMediaServer(object):
def query(self, *parameters):
"""Send request and await response from server."""
try:
telnet = telnetlib.Telnet(self.host, self.port)
if self._username and self._password:
telnet.write('login {username} {password}\n'.format(
username=self._username,
password=self._password).encode('UTF-8'))
telnet.read_until(b'\n', timeout=3)
message = '{}\n'.format(' '.join(parameters))
telnet.write(message.encode('UTF-8'))
response = telnet.read_until(b'\n', timeout=3)\
.decode('UTF-8')\
.split(' ')[-1]\
.strip()
telnet.write(b'exit\n')
return urllib.parse.unquote(response)
except (OSError, ConnectionError) as error:
_LOGGER.error("Could not communicate with %s:%d: %s",
self.host,
self.port,
error)
return None
response = urllib.parse.unquote(self.get(' '.join(parameters)))
return response.split(' ')[-1].strip()
def get_player_status(self, player):
"""Get ithe status of a player."""
"""Get the status of a player."""
# (title) : Song title
# Requested Information
# a (artist): Artist name 'artist'
# d (duration): Song duration in seconds 'duration'
# K (artwork_url): URL to remote artwork
tags = 'adK'
# l (album): Album, including the server's "(N of M)"
tags = 'adKl'
new_status = {}
response = self.get('{player} status - 1 tags:{tags}\n'
.format(player=player, tags=tags))
if not response:
return {}
response = response.split(' ')
for item in response:
parts = urllib.parse.unquote(item).partition(':')
new_status[parts[0]] = parts[2]
return new_status
def get(self, command):
"""Abstract out the telnet connection."""
try:
telnet = telnetlib.Telnet(self.host, self.port)
telnet.write('{player} status - 1 tags:{tags}\n'.format(
player=player,
tags=tags
).encode('UTF-8'))
if self._username and self._password:
_LOGGER.debug("Logging in")
telnet.write('login {username} {password}\n'.format(
username=self._username,
password=self._password).encode('UTF-8'))
telnet.read_until(b'\n', timeout=3)
_LOGGER.debug("About to send message: %s", command)
message = '{}\n'.format(command)
telnet.write(message.encode('UTF-8'))
response = telnet.read_until(b'\n', timeout=3)\
.decode('UTF-8')\
.split(' ')
telnet.write(b'exit\n')
for item in response:
parts = urllib.parse.unquote(item).partition(':')
new_status[parts[0]] = parts[2]
except (OSError, ConnectionError) as error:
_LOGGER.debug("Response: %s", response)
return response
except (OSError, ConnectionError, EOFError) as error:
_LOGGER.error("Could not communicate with %s:%d: %s",
self.host,
self.port,
error)
return new_status
return None
# pylint: disable=too-many-instance-attributes
@@ -227,23 +249,44 @@ class SqueezeBoxDevice(MediaPlayerDevice):
media_url = ('/music/current/cover.jpg?player={player}').format(
player=self._id)
base_url = 'http://{server}:{port}/'.format(
server=self._lms.host,
port=self._lms.http_port)
# pylint: disable=protected-access
if self._lms._username:
base_url = 'http://{username}:{password}@{server}:{port}/'.format(
username=self._lms._username,
password=self._lms._password,
server=self._lms.host,
port=self._lms.http_port)
else:
base_url = 'http://{server}:{port}/'.format(
server=self._lms.host,
port=self._lms.http_port)
return urllib.parse.urljoin(base_url, media_url)
url = urllib.parse.urljoin(base_url, media_url)
_LOGGER.debug("Media image url: %s", url)
return url
@property
def media_title(self):
"""Title of current playing media."""
if 'artist' in self._status and 'title' in self._status:
return '{artist} - {title}'.format(
artist=self._status['artist'],
title=self._status['title']
)
if 'title' in self._status:
return self._status['title']
if 'current_title' in self._status:
return self._status['current_title']
@property
def media_artist(self):
"""Artist of current playing media."""
if 'artist' in self._status:
return self._status['artist']
@property
def media_album_name(self):
"""Album of current playing media."""
if 'album' in self._status:
return self._status['album'].rstrip()
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
@@ -326,7 +369,7 @@ class SqueezeBoxDevice(MediaPlayerDevice):
"""
Replace the current play list with the uri.
Telnet Command Strucutre:
Telnet Command Structure:
<playerid> playlist play <item> <title> <fadeInSecs>
The "playlist play" command puts the specified song URL,
@@ -350,7 +393,7 @@ class SqueezeBoxDevice(MediaPlayerDevice):
"""
Add a items to the existing playlist.
Telnet Command Strucutre:
Telnet Command Structure:
<playerid> playlist add <item>
The "playlist add" command adds the specified song URL, playlist or
@@ -10,16 +10,19 @@ import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA)
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA,
MEDIA_TYPE_MUSIC,
MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['rxv==0.1.11']
REQUIREMENTS = ['rxv==0.2.0']
_LOGGER = logging.getLogger(__name__)
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | \
SUPPORT_PLAY_MEDIA
CONF_SOURCE_NAMES = 'source_names'
CONF_SOURCE_IGNORE = 'source_ignore'
@@ -45,11 +48,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
source_names = config.get(CONF_SOURCE_NAMES)
if host is None:
receivers = rxv.find()
receivers = []
for recv in rxv.find():
receivers.extend(recv.zone_controllers())
else:
receivers = \
[rxv.RXV("http://{}:80/YamahaRemoteControl/ctrl".format(host),
name)]
ctrl_url = "http://{}:80/YamahaRemoteControl/ctrl".format(host)
receivers = rxv.RXV(ctrl_url, name).zone_controllers()
add_devices(
YamahaDevice(name, receiver, source_ignore, source_names)
@@ -74,6 +78,7 @@ class YamahaDevice(MediaPlayerDevice):
self._reverse_mapping = None
self.update()
self._name = name
self._zone = receiver.zone
def update(self):
"""Get the latest details from the device."""
@@ -104,7 +109,11 @@ class YamahaDevice(MediaPlayerDevice):
@property
def name(self):
"""Return the name of the device."""
return self._name
name = self._name
if self._zone != "Main_Zone":
# Zone will be one of Main_Zone, Zone_2, Zone_3
name += " " + self._zone.replace('_', ' ')
return name
@property
def state(self):
@@ -158,3 +167,35 @@ class YamahaDevice(MediaPlayerDevice):
def select_source(self, source):
"""Select input source."""
self._receiver.input = self._reverse_mapping.get(source, source)
def play_media(self, media_type, media_id, **kwargs):
"""Play media from an ID.
This exposes a pass through for various input sources in the
Yamaha to direct play certain kinds of media. media_type is
treated as the input type that we are setting, and media id is
specific to it.
"""
if media_type == "NET RADIO":
self._receiver.net_radio(media_id)
@property
def media_content_type(self):
"""Return the media content type."""
if self.source == "NET RADIO":
return MEDIA_TYPE_MUSIC
@property
def media_title(self):
"""Return the media title.
This will vary by input source, as they provide different
information in metadata.
"""
if self.source == "NET RADIO":
info = self._receiver.play_status()
if info.song:
return "%s: %s" % (info.station, info.song)
else:
return info.station
+19 -14
View File
@@ -18,8 +18,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import template, config_validation as cv
from homeassistant.helpers.event import threaded_listener_factory
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_VALUE_TEMPLATE)
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE)
_LOGGER = logging.getLogger(__name__)
@@ -107,12 +106,11 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
MQTT_BASE_PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Optional(CONF_SCAN_INTERVAL):
vol.All(vol.Coerce(int), vol.Range(min=1)),
SCHEMA_BASE = {
vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
})
}
MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE)
# Sensor type platforms subscribe to MQTT events
MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({
@@ -401,13 +399,20 @@ class MQTT(object):
def _mqtt_on_message(self, _mqttc, _userdata, msg):
"""Message received callback."""
_LOGGER.debug("received message on %s: %s",
msg.topic, msg.payload.decode('utf-8'))
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
ATTR_TOPIC: msg.topic,
ATTR_QOS: msg.qos,
ATTR_PAYLOAD: msg.payload.decode('utf-8'),
})
try:
payload = msg.payload.decode('utf-8')
except AttributeError:
_LOGGER.error("Illegal utf-8 unicode payload from "
"MQTT topic: %s, Payload: %s", msg.topic,
msg.payload)
else:
_LOGGER.debug("received message on %s: %s",
msg.topic, payload)
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
ATTR_TOPIC: msg.topic,
ATTR_QOS: msg.qos,
ATTR_PAYLOAD: payload,
})
def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos):
"""Unsubscribe successful callback."""
+1 -1
View File
@@ -42,7 +42,7 @@ GATEWAYS = None
MQTT_COMPONENT = 'mqtt'
REQUIREMENTS = [
'https://github.com/theolind/pymysensors/archive/'
'8ce98b7fb56f7921a808eb66845ce8b2c455c81e.zip#pymysensors==0.7.1']
'0b705119389be58332f17753c53167f551254b6c.zip#pymysensors==0.8']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
+1 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-nest==2.10.0']
REQUIREMENTS = ['python-nest==2.11.0']
DOMAIN = 'nest'
+44 -6
View File
@@ -1,22 +1,24 @@
"""
Support for the Netatmo devices (Weather Station and Welcome camera).
Support for the Netatmo devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/netatmo/
"""
import logging
from datetime import timedelta
from urllib.error import HTTPError
import voluptuous as vol
from homeassistant.const import (
CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME)
CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = [
'https://github.com/jabesq/netatmo-api-python/archive/'
'v0.5.0.zip#lnetatmo==0.5.0']
'v0.6.0.zip#lnetatmo==0.6.0']
_LOGGER = logging.getLogger(__name__)
@@ -25,6 +27,9 @@ CONF_SECRET_KEY = 'secret_key'
DOMAIN = 'netatmo'
NETATMO_AUTH = None
DEFAULT_DISCOVERY = True
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -32,6 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_SECRET_KEY): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean,
})
}, extra=vol.ALLOW_EXTRA)
@@ -45,12 +51,44 @@ def setup(hass, config):
NETATMO_AUTH = lnetatmo.ClientAuth(
config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY],
config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD],
'read_station read_camera access_camera')
'read_station read_camera access_camera '
'read_thermostat write_thermostat')
except HTTPError:
_LOGGER.error("Unable to connect to Netatmo API")
return False
for component in 'camera', 'sensor':
discovery.load_platform(hass, component, DOMAIN, {}, config)
if config[DOMAIN][CONF_DISCOVERY]:
for component in 'camera', 'sensor', 'binary_sensor', 'climate':
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
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.camera_names = []
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)
+289
View File
@@ -0,0 +1,289 @@
"""
APNS Notification platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.apns/
"""
import logging
import os
import voluptuous as vol
from homeassistant.helpers.event import track_state_change
from homeassistant.config import load_yaml_config_file
from homeassistant.components.notify import (
ATTR_TARGET, ATTR_DATA, BaseNotificationService)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import template as template_helper
DOMAIN = "apns"
APNS_DEVICES = "apns.yaml"
DEVICE_TRACKER_DOMAIN = "device_tracker"
SERVICE_REGISTER = "apns_register"
ATTR_PUSH_ID = "push_id"
ATTR_NAME = "name"
REGISTER_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_PUSH_ID): cv.string,
vol.Optional(ATTR_NAME, default=None): cv.string,
})
REQUIREMENTS = ["apns2==0.1.1"]
def get_service(hass, config):
"""Return push service."""
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
name = config.get("name")
if name is None:
logging.error("Name must be specified.")
return None
cert_file = config.get('cert_file')
if cert_file is None:
logging.error("Certificate must be specified.")
return None
topic = config.get('topic')
if topic is None:
logging.error("Topic must be specified.")
return None
sandbox = bool(config.get('sandbox', False))
service = ApnsNotificationService(hass, name, topic, sandbox, cert_file)
hass.services.register(DOMAIN,
name,
service.register,
descriptions.get(SERVICE_REGISTER),
schema=REGISTER_SERVICE_SCHEMA)
return service
class ApnsDevice(object):
"""
Apns Device class.
Stores information about a device that is
registered for push notifications.
"""
def __init__(self, push_id, name, tracking_device_id=None, disabled=False):
"""Initialize Apns Device."""
self.device_push_id = push_id
self.device_name = name
self.tracking_id = tracking_device_id
self.device_disabled = disabled
@property
def push_id(self):
"""The apns id for the device."""
return self.device_push_id
@property
def name(self):
"""The friendly name for the device."""
return self.device_name
@property
def tracking_device_id(self):
"""
Device Id.
The id of a device that is tracked by the device
tracking component.
"""
return self.tracking_id
@property
def full_tracking_device_id(self):
"""
Fully qualified device id.
The full id of a device that is tracked by the device
tracking component.
"""
return DEVICE_TRACKER_DOMAIN + '.' + self.tracking_id
@property
def disabled(self):
"""Should receive notifications."""
return self.device_disabled
def disable(self):
"""Disable the device from recieving notifications."""
self.device_disabled = True
def __eq__(self, other):
"""Return the comparision."""
if isinstance(other, self.__class__):
return self.push_id == other.push_id and self.name == other.name
return NotImplemented
def __ne__(self, other):
"""Return the comparision."""
return not self.__eq__(other)
class ApnsNotificationService(BaseNotificationService):
"""Implement the notification service for the APNS service."""
# pylint: disable=too-many-arguments
# pylint: disable=too-many-instance-attributes
def __init__(self, hass, app_name, topic, sandbox, cert_file):
"""Initialize APNS application."""
self.hass = hass
self.app_name = app_name
self.sandbox = sandbox
self.certificate = cert_file
self.yaml_path = hass.config.path(app_name + '_' + APNS_DEVICES)
self.devices = {}
self.device_states = {}
self.topic = topic
if os.path.isfile(self.yaml_path):
self.devices = {
str(key): ApnsDevice(
str(key),
value.get('name'),
value.get('tracking_device_id'),
value.get('disabled', False)
)
for (key, value) in
load_yaml_config_file(self.yaml_path).items()
}
tracking_ids = [
device.full_tracking_device_id
for (key, device) in self.devices.items()
if device.tracking_device_id is not None
]
track_state_change(
hass,
tracking_ids,
self.device_state_changed_listener)
def device_state_changed_listener(self, entity_id, from_s, to_s):
"""
Listener for sate change.
Track device state change if a device
has a tracking id specified.
"""
self.device_states[entity_id] = str(to_s.state)
return
@staticmethod
def write_device(out, device):
"""Write a single device to file."""
attributes = []
if device.name is not None:
attributes.append(
'name: {}'.format(device.name))
if device.tracking_device_id is not None:
attributes.append(
'tracking_device_id: {}'.format(device.tracking_device_id))
if device.disabled:
attributes.append('disabled: True')
out.write(device.push_id)
out.write(": {")
if len(attributes) > 0:
separator = ", "
out.write(separator.join(attributes))
out.write("}\n")
def write_devices(self):
"""Write all known devices to file."""
with open(self.yaml_path, 'w+') as out:
for _, device in self.devices.items():
ApnsNotificationService.write_device(out, device)
def register(self, call):
"""Register a device to receive push messages."""
push_id = call.data.get(ATTR_PUSH_ID)
if push_id is None:
return False
device_name = call.data.get(ATTR_NAME)
current_device = self.devices.get(push_id)
current_tracking_id = None if current_device is None \
else current_device.tracking_device_id
device = ApnsDevice(
push_id,
device_name,
current_tracking_id)
if current_device is None:
self.devices[push_id] = device
with open(self.yaml_path, 'a') as out:
self.write_device(out, device)
return True
if device != current_device:
self.devices[push_id] = device
self.write_devices()
return True
def send_message(self, message=None, **kwargs):
"""Send push message to registered devices."""
from apns2.client import APNsClient
from apns2.payload import Payload
from apns2.errors import Unregistered
apns = APNsClient(
self.certificate,
use_sandbox=self.sandbox,
use_alternative_port=False)
device_state = kwargs.get(ATTR_TARGET)
message_data = kwargs.get(ATTR_DATA)
if message_data is None:
message_data = {}
if isinstance(message, str):
rendered_message = message
elif isinstance(message, template_helper.Template):
rendered_message = message.render()
else:
rendered_message = ""
payload = Payload(
alert=rendered_message,
badge=message_data.get("badge"),
sound=message_data.get("sound"),
category=message_data.get("category"),
custom=message_data.get("custom", {}),
content_available=message_data.get("content_available", False))
device_update = False
for push_id, device in self.devices.items():
if not device.disabled:
state = None
if device.tracking_device_id is not None:
state = self.device_states.get(
device.full_tracking_device_id)
if device_state is None or state == str(device_state):
try:
apns.send_notification(
push_id,
payload,
topic=self.topic)
except Unregistered:
logging.error(
"Device %s has unregistered.",
push_id)
device_update = True
device.disable()
if device_update:
self.write_devices()
return True
+87
View File
@@ -0,0 +1,87 @@
"""
iOS push notification platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.ios/
"""
import logging
from datetime import datetime, timezone
import requests
from homeassistant.components import ios
import homeassistant.util.dt as dt_util
from homeassistant.components.notify import (
ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_MESSAGE,
ATTR_DATA, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
PUSH_URL = "https://ios-push.home-assistant.io/push"
DEPENDENCIES = ["ios"]
def get_service(hass, config):
"""Get the iOS notification service."""
if "notify.ios" not in hass.config.components:
# Need this to enable requirements checking in the app.
hass.config.components.append("notify.ios")
return iOSNotificationService()
# pylint: disable=too-few-public-methods, too-many-arguments, invalid-name
class iOSNotificationService(BaseNotificationService):
"""Implement the notification service for iOS."""
def __init__(self):
"""Initialize the service."""
@property
def targets(self):
"""Return a dictionary of registered targets."""
return ios.devices_with_push()
def send_message(self, message="", **kwargs):
"""Send a message to the Lambda APNS gateway."""
data = {ATTR_MESSAGE: message}
if kwargs.get(ATTR_TITLE) is not None:
# Remove default title from notifications.
if kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT:
data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)
targets = kwargs.get(ATTR_TARGET)
if not targets:
targets = ios.enabled_push_ids()
if kwargs.get(ATTR_DATA) is not None:
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
for target in targets:
data[ATTR_TARGET] = target
req = requests.post(PUSH_URL, json=data, timeout=10)
if req.status_code is not 201:
message = req.json()["message"]
if req.status_code is 429:
_LOGGER.warning(message)
elif req.status_code is 400 or 500:
_LOGGER.error(message)
if req.status_code in (201, 429):
rate_limits = req.json()["rateLimits"]
resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"])
resetsAtTime = resetsAt - datetime.now(timezone.utc)
rate_limit_msg = ("iOS push notification rate limits for %s: "
"%d sent, %d allowed, %d errors, "
"resets in %s")
_LOGGER.info(rate_limit_msg,
ios.device_name_for_push_id(target),
rate_limits["successful"],
rate_limits["maximum"], rate_limits["errors"],
str(resetsAtTime).split(".")[0])
+169
View File
@@ -0,0 +1,169 @@
"""
Matrix notification service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.matrix/
"""
import logging
import json
import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL
REQUIREMENTS = ['matrix-client==0.0.5']
SESSION_FILE = 'matrix.conf'
AUTH_TOKENS = dict()
CONF_HOMESERVER = 'homeserver'
CONF_DEFAULT_ROOM = 'default_room'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOMESERVER): cv.url,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_DEFAULT_ROOM): cv.string,
})
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config):
"""Get the Matrix notification service."""
if not AUTH_TOKENS:
load_token(hass.config.path(SESSION_FILE))
return MatrixNotificationService(
config.get(CONF_HOMESERVER),
config.get(CONF_DEFAULT_ROOM),
config.get(CONF_VERIFY_SSL),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD)
)
# pylint: disable=too-few-public-methods
class MatrixNotificationService(BaseNotificationService):
"""Wrapper for the MatrixNotificationClient."""
# pylint: disable=too-many-arguments
def __init__(self, homeserver, default_room, verify_ssl,
username, password):
"""Buffer configuration data for send_message."""
self.homeserver = homeserver
self.default_room = default_room
self.verify_tls = verify_ssl
self.username = username
self.password = password
def send_message(self, message, **kwargs):
"""Wrapper function pass default parameters to actual send_message."""
send_message(
message,
self.homeserver,
kwargs.get(ATTR_TARGET) or [self.default_room],
self.verify_tls,
self.username,
self.password
)
def load_token(session_file):
"""Load authentication tokens from persistent storage, if exists."""
if not os.path.exists(session_file):
return
with open(session_file) as handle:
data = json.load(handle)
for mx_id, token in data.items():
AUTH_TOKENS[mx_id] = token
def store_token(mx_id, token):
"""Store authentication token to session and persistent storage."""
AUTH_TOKENS[mx_id] = token
with open(SESSION_FILE, 'w') as handle:
handle.write(json.dumps(AUTH_TOKENS))
# pylint: disable=too-many-locals, too-many-arguments
def send_message(message, homeserver, target_rooms, verify_tls,
username, password):
"""Do everything thats necessary to send a message to a Matrix room."""
from matrix_client.client import MatrixClient, MatrixRequestError
def login_by_token():
"""Login using authentication token."""
try:
return MatrixClient(
base_url=homeserver,
token=AUTH_TOKENS[mx_id],
user_id=username,
valid_cert_check=verify_tls
)
except MatrixRequestError as ex:
_LOGGER.info(
'login_by_token: (%d) %s', ex.code, ex.content
)
def login_by_password():
"""Login using password authentication."""
try:
_client = MatrixClient(
base_url=homeserver,
valid_cert_check=verify_tls
)
_client.login_with_password(username, password)
store_token(mx_id, _client.token)
return _client
except MatrixRequestError as ex:
_LOGGER.error(
'login_by_password: (%d) %s', ex.code, ex.content
)
# this is as close as we can get to the mx_id, since there is no
# homeserver discovery protocol we have to fall back to the homeserver url
# instead of the actual domain it serves.
mx_id = "{user}@{homeserver}".format(
user=username,
homeserver=homeserver
)
if mx_id in AUTH_TOKENS:
client = login_by_token()
if not client:
client = login_by_password()
if not client:
_LOGGER.error(
'login failed, both token and username/password '
'invalid'
)
return
else:
client = login_by_password()
if not client:
_LOGGER.error('login failed, username/password invalid')
return
rooms = client.get_rooms()
for target_room in target_rooms:
try:
if target_room in rooms:
room = rooms[target_room]
else:
room = client.join_room(target_room)
_LOGGER.debug(room.send_text(message))
except MatrixRequestError as ex:
_LOGGER.error(
'Unable to deliver message to room \'%s\': (%d): %s',
target_room, ex.code, ex.content
)
+22 -6
View File
@@ -9,14 +9,15 @@ import logging
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA,
BaseNotificationService)
ATTR_DATA, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT,
PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pushbullet.py==0.10.0']
ATTR_URL = 'url'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
@@ -40,7 +41,7 @@ def get_service(hass, config):
return PushBulletNotificationService(pushbullet)
# pylint: disable=too-few-public-methods
# pylint: disable=too-few-public-methods, too-many-branches
class PushBulletNotificationService(BaseNotificationService):
"""Implement the notification service for Pushbullet."""
@@ -79,11 +80,18 @@ class PushBulletNotificationService(BaseNotificationService):
"""
targets = kwargs.get(ATTR_TARGET)
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = kwargs.get(ATTR_DATA)
url = None
if data:
url = data.get(ATTR_URL, None)
refreshed = False
if not targets:
# Backward compatebility, notify all devices in own account
self.pushbullet.push_note(title, message)
if url:
self.pushbullet.push_link(title, url, body=message)
else:
self.pushbullet.push_note(title, message)
_LOGGER.info('Sent notification to self')
return
@@ -98,7 +106,11 @@ class PushBulletNotificationService(BaseNotificationService):
# Target is email, send directly, don't use a target object
# This also seems works to send to all devices in own account
if ttype == 'email':
self.pushbullet.push_note(title, message, email=tname)
if url:
self.pushbullet.push_link(title, url,
body=message, email=tname)
else:
self.pushbullet.push_note(title, message, email=tname)
_LOGGER.info('Sent notification to email %s', tname)
continue
@@ -117,7 +129,11 @@ class PushBulletNotificationService(BaseNotificationService):
# Attempt push_note on a dict value. Keys are types & target
# name. Dict pbtargets has all *actual* targets.
try:
self.pbtargets[ttype][tname].push_note(title, message)
if url:
self.pbtargets[ttype][tname].push_link(title, url,
body=message)
else:
self.pbtargets[ttype][tname].push_note(title, message)
_LOGGER.info('Sent notification to %s/%s', ttype, tname)
except KeyError:
_LOGGER.error('No such target: %s/%s', ttype, tname)
+1 -1
View File
@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['sendgrid==3.4.0']
REQUIREMENTS = ['sendgrid==3.6.0']
_LOGGER = logging.getLogger(__name__)
@@ -17,3 +17,15 @@ notify:
data:
description: Extended information for notification. Optional depending on the platform.
example: platform specific
apns_register:
description: Registers a device to receive push notifications.
fields:
push_id:
description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service.
example: '72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62'
name:
description: A friendly name for the device (optional).
example: 'Sam''s iPhone'
+1 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.const import (
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['slacker==0.9.25']
REQUIREMENTS = ['slacker==0.9.28']
_LOGGER = logging.getLogger(__name__)
+109
View File
@@ -0,0 +1,109 @@
"""
Telstra API platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.telstra/
"""
import logging
import requests
import voluptuous as vol
from homeassistant.components.notify import (BaseNotificationService,
ATTR_TITLE,
PLATFORM_SCHEMA)
from homeassistant.const import CONTENT_TYPE_JSON
import homeassistant.helpers.config_validation as cv
CONF_CONSUMER_KEY = 'consumer_key'
CONF_CONSUMER_SECRET = 'consumer_secret'
CONF_PHONE_NUMBER = 'phone_number'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CONSUMER_KEY): cv.string,
vol.Required(CONF_CONSUMER_SECRET): cv.string,
vol.Required(CONF_PHONE_NUMBER): cv.string,
})
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config):
"""Get the Telstra SMS API notification service."""
consumer_key = config.get(CONF_CONSUMER_KEY)
consumer_secret = config.get(CONF_CONSUMER_SECRET)
phone_number = config.get(CONF_PHONE_NUMBER)
# Attempt an initial authentication to confirm credentials
if _authenticate(consumer_key, consumer_secret) is False:
_LOGGER.exception('Error obtaining authorization from Telstra API')
return None
return TelstraNotificationService(consumer_key,
consumer_secret,
phone_number)
# pylint: disable=too-few-public-methods, too-many-arguments
class TelstraNotificationService(BaseNotificationService):
"""Implementation of a notification service for the Telstra SMS API."""
def __init__(self, consumer_key, consumer_secret, phone_number):
"""Initialize the service."""
self._consumer_key = consumer_key
self._consumer_secret = consumer_secret
self._phone_number = phone_number
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
title = kwargs.get(ATTR_TITLE)
# Retrieve authorization first
token_response = _authenticate(self._consumer_key,
self._consumer_secret)
if token_response is False:
_LOGGER.exception('Error obtaining authorization from Telstra API')
return
# Send the SMS
if title:
text = '{} {}'.format(title, message)
else:
text = message
message_data = {
'to': self._phone_number,
'body': text
}
message_resource = 'https://api.telstra.com/v1/sms/messages'
message_headers = {
'Content-Type': CONTENT_TYPE_JSON,
'Authorization': 'Bearer ' + token_response['access_token']
}
message_response = requests.post(message_resource,
headers=message_headers,
json=message_data,
timeout=10)
if message_response.status_code != 202:
_LOGGER.exception("Failed to send SMS. Status code: %d",
message_response.status_code)
def _authenticate(consumer_key, consumer_secret):
"""Authenticate with the Telstra API."""
token_data = {
'client_id': consumer_key,
'client_secret': consumer_secret,
'grant_type': 'client_credentials',
'scope': 'SMS'
}
token_resource = 'https://api.telstra.com/v1/oauth/token'
token_response = requests.get(token_resource,
params=token_data,
timeout=10).json()
if 'error' in token_response:
return False
return token_response
+2 -4
View File
@@ -14,10 +14,11 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
REQUIREMENTS = ['sleekxmpp==1.3.1',
'dnspython3==1.14.0',
'dnspython3==1.15.0',
'pyasn1==0.1.9',
'pyasn1-modules==0.0.8']
_LOGGER = logging.getLogger(__name__)
CONF_TLS = 'tls'
@@ -29,9 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config):
"""Get the Jabber (XMPP) notification service."""
return XmppNotificationService(
+1 -4
View File
@@ -79,8 +79,7 @@ class NuimoThread(threading.Thread):
self._name = name
self._hass_is_running = True
self._nuimo = None
self._listener = hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
self.stop)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
def run(self):
"""Setup connection or be idle."""
@@ -99,8 +98,6 @@ class NuimoThread(threading.Thread):
"""Terminate Thread by unsetting flag."""
_LOGGER.debug('Stopping thread for Nuimo %s', self._mac)
self._hass_is_running = False
self._hass.bus.remove_listener(EVENT_HOMEASSISTANT_STOP,
self._listener)
def _attach(self):
"""Create a nuimo object from mac address or discovery."""
+2 -2
View File
@@ -14,7 +14,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT,
CONF_WHITELIST)
REQUIREMENTS = ['pilight==0.0.2']
REQUIREMENTS = ['pilight==0.1.1']
_LOGGER = logging.getLogger(__name__)
@@ -102,7 +102,7 @@ def setup(hass, config):
if not whitelist:
hass.bus.fire(EVENT, data)
# Check if data matches the defined whitelist
elif all(data[key] in whitelist[key] for key in whitelist):
elif all(str(data[key]) in whitelist[key] for key in whitelist):
hass.bus.fire(EVENT, data)
pilight_client.set_callback(handle_received_code)
+59 -73
View File
@@ -9,106 +9,92 @@ https://home-assistant.io/components/proximity/
"""
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_ZONE, CONF_DEVICES, CONF_UNIT_OF_MEASUREMENT)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_state_change
from homeassistant.util.location import distance
from homeassistant.util.distance import convert
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
DEPENDENCIES = ['zone', 'device_tracker']
DOMAIN = 'proximity'
NOT_SET = 'not set'
# Default tolerance
DEFAULT_TOLERANCE = 1
# Default zone
DEFAULT_PROXIMITY_ZONE = 'home'
# Default distance to zone
DEFAULT_DIST_TO_ZONE = NOT_SET
# Default direction of travel
DEFAULT_DIR_OF_TRAVEL = NOT_SET
# Default nearest device
DEFAULT_NEAREST = NOT_SET
# Entity attributes
ATTR_DIST_FROM = 'dist_to_zone'
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
ATTR_NEAREST = 'nearest'
from homeassistant.util.location import distance
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
ATTR_DIST_FROM = 'dist_to_zone'
ATTR_NEAREST = 'nearest'
def setup_proximity_component(hass, config):
CONF_IGNORED_ZONES = 'ignored_zones'
CONF_TOLERANCE = 'tolerance'
DEFAULT_DIR_OF_TRAVEL = 'not set'
DEFAULT_DIST_TO_ZONE = 'not set'
DEFAULT_NEAREST = 'not set'
DEFAULT_PROXIMITY_ZONE = 'home'
DEFAULT_TOLERANCE = 1
DEPENDENCIES = ['zone', 'device_tracker']
DOMAIN = 'proximity'
UNITS = ['km', 'm', 'mi', 'ft']
ZONE_SCHEMA = vol.Schema({
vol.Optional(CONF_ZONE, default=DEFAULT_PROXIMITY_ZONE): cv.string,
vol.Optional(CONF_DEVICES, default=[]):
vol.All(cv.ensure_list, [cv.entity_id]),
vol.Optional(CONF_IGNORED_ZONES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): cv.positive_int,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.All(cv.string, vol.In(UNITS)),
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: ZONE_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
def setup_proximity_component(hass, name, config):
"""Set up individual proximity component."""
# Get the devices from configuration.yaml.
if 'devices' not in config:
_LOGGER.error('devices not found in config')
return False
ignored_zones = []
if 'ignored_zones' in config:
for variable in config['ignored_zones']:
ignored_zones.append(variable)
proximity_devices = []
for variable in config['devices']:
proximity_devices.append(variable)
# Get the direction of travel tolerance from configuration.yaml.
tolerance = config.get('tolerance', DEFAULT_TOLERANCE)
# Get the zone to monitor proximity to from configuration.yaml.
proximity_zone = config.get('zone', DEFAULT_PROXIMITY_ZONE)
# Get the unit of measurement from configuration.yaml.
unit_of_measure = config.get(ATTR_UNIT_OF_MEASUREMENT,
hass.config.units.length_unit)
ignored_zones = config.get(CONF_IGNORED_ZONES)
proximity_devices = config.get(CONF_DEVICES)
tolerance = config.get(CONF_TOLERANCE)
proximity_zone = name
unit_of_measurement = config.get(
CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit)
zone_id = 'zone.{}'.format(proximity_zone)
state = hass.states.get(zone_id)
zone_friendly_name = (state.name).lower()
proximity = Proximity(hass, zone_friendly_name, DEFAULT_DIST_TO_ZONE,
proximity = Proximity(hass, proximity_zone, DEFAULT_DIST_TO_ZONE,
DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST,
ignored_zones, proximity_devices, tolerance,
zone_id, unit_of_measure)
zone_id, unit_of_measurement)
proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone)
proximity.update_ha_state()
# Main command to monitor proximity of devices.
track_state_change(hass, proximity_devices,
proximity.check_proximity_state_change)
track_state_change(
hass, proximity_devices, proximity.check_proximity_state_change)
return True
def setup(hass, config):
"""Get the zones and offsets from configuration.yaml."""
result = True
if isinstance(config[DOMAIN], list):
for proximity_config in config[DOMAIN]:
if not setup_proximity_component(hass, proximity_config):
result = False
elif not setup_proximity_component(hass, config[DOMAIN]):
result = False
for zone, proximity_config in config[DOMAIN].items():
setup_proximity_component(hass, zone, proximity_config)
return result
return True
class Proximity(Entity): # pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-instance-attributes
class Proximity(Entity):
"""Representation of a Proximity."""
# pylint: disable=too-many-arguments
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
nearest, ignored_zones, proximity_devices, tolerance,
proximity_zone, unit_of_measure):
proximity_zone, unit_of_measurement):
"""Initialize the proximity."""
self.hass = hass
self.friendly_name = zone_friendly_name
@@ -119,7 +105,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
self.proximity_devices = proximity_devices
self.tolerance = tolerance
self.proximity_zone = proximity_zone
self.unit_of_measure = unit_of_measure
self._unit_of_measurement = unit_of_measurement
@property
def name(self):
@@ -134,7 +120,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return self.unit_of_measure
return self._unit_of_measurement
@property
def state_attributes(self):
@@ -209,7 +195,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
# Add the device and distance to a dictionary.
distances_to_zone[device] = round(
convert(dist_to_zone, 'm', self.unit_of_measure), 1)
convert(dist_to_zone, 'm', self.unit_of_measurement), 1)
# Loop through each of the distances collected and work out the
# closest.
+12 -14
View File
@@ -7,7 +7,6 @@ to query this database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/recorder/
"""
import asyncio
import logging
import queue
import threading
@@ -17,7 +16,7 @@ from typing import Any, Union, Optional, List
import voluptuous as vol
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED, MATCH_ALL)
@@ -26,15 +25,15 @@ from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType, QueryType
import homeassistant.util.dt as dt_util
DOMAIN = "recorder"
DOMAIN = 'recorder'
REQUIREMENTS = ['sqlalchemy==1.0.15']
REQUIREMENTS = ['sqlalchemy==1.1.1']
DEFAULT_URL = "sqlite:///{hass_config_path}"
DEFAULT_DB_FILE = "home-assistant_v2.db"
DEFAULT_URL = 'sqlite:///{hass_config_path}'
DEFAULT_DB_FILE = 'home-assistant_v2.db'
CONF_DB_URL = "db_url"
CONF_PURGE_DAYS = "purge_days"
CONF_DB_URL = 'db_url'
CONF_PURGE_DAYS = 'purge_days'
RETRIES = 3
CONNECT_RETRY_WAIT = 10
@@ -56,8 +55,8 @@ _LOGGER = logging.getLogger(__name__)
Session = None # pylint: disable=no-member
def execute(q: QueryType) \
-> List[Any]: # pylint: disable=invalid-sequence-index
# pylint: disable=invalid-sequence-index
def execute(q: QueryType) -> List[Any]:
"""Query the database and convert the objects to HA native form.
This method also retries a few times in the case of stale connections.
@@ -101,7 +100,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
global _INSTANCE # pylint: disable=global-statement
if _INSTANCE is not None:
_LOGGER.error('Only a single instance allowed.')
_LOGGER.error("Only a single instance allowed")
return False
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
@@ -155,8 +154,7 @@ class Recorder(threading.Thread):
"""A threaded recorder class."""
# pylint: disable=too-many-instance-attributes
def __init__(self, hass: HomeAssistant, purge_days: int, uri: str) \
-> None:
def __init__(self, hass: HomeAssistant, purge_days: int, uri: str) -> None:
"""Initialize the recorder."""
threading.Thread.__init__(self)
@@ -226,7 +224,7 @@ class Recorder(threading.Thread):
self.queue.task_done()
@asyncio.coroutine
@callback
def event_listener(self, event):
"""Listen for new events and put them in the process queue."""
self.queue.put(event)
@@ -11,8 +11,9 @@ from homeassistant.helpers.entity import generate_entity_id
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = [
'https://github.com/sander76/powerviewApi/'
'archive/cc6f75dd39160d4aaf46cb2ed9220136b924bcb4.zip#powerviewApi==0.2']
'https://github.com/sander76/powerviewApi/archive'
'/246e782d60d5c0addcc98d7899a0186f9d5640b0.zip#powerviewApi==0.3.15'
]
HUB_ADDRESS = 'address'
@@ -20,7 +21,7 @@ HUB_ADDRESS = 'address'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the powerview scenes stored in a Powerview hub."""
import powerview
from powerview_api import powerview
hub_address = config.get(HUB_ADDRESS)
+26 -10
View File
@@ -8,28 +8,44 @@ https://home-assistant.io/components/sensor.arduino/
"""
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.components.arduino as arduino
from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.const import CONF_NAME
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_PINS = 'pins'
CONF_TYPE = 'analog'
DEPENDENCIES = ['arduino']
_LOGGER = logging.getLogger(__name__)
PIN_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PINS):
vol.Schema({cv.positive_int: PIN_SCHEMA}),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Arduino platform."""
"""Set up the Arduino platform."""
# Verify that the Arduino board is present
if arduino.BOARD is None:
_LOGGER.error('A connection has not been made to the Arduino board.')
_LOGGER.error("A connection has not been made to the Arduino board")
return False
pins = config.get(CONF_PINS)
sensors = []
pins = config.get('pins')
for pinnum, pin in pins.items():
if pin.get('name'):
sensors.append(ArduinoSensor(pin.get('name'),
pinnum,
'analog'))
sensors.append(ArduinoSensor(pin.get(CONF_NAME), pinnum, CONF_TYPE))
add_devices(sensors)
@@ -39,7 +55,7 @@ class ArduinoSensor(Entity):
def __init__(self, name, pin, pin_type):
"""Initialize the sensor."""
self._pin = pin
self._name = name or DEVICE_DEFAULT_NAME
self._name = name
self.pin_type = pin_type
self.direction = 'in'
self._value = None
+126
View File
@@ -0,0 +1,126 @@
"""Support for collecting data from the ARWN project.
For more details about this platform, please refer to the
documentation at https://home-assistant.io/components/sensor.arwn/
"""
import json
import logging
from homeassistant.helpers.entity import Entity
import homeassistant.components.mqtt as mqtt
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
from homeassistant.util import slugify
DEPENDENCIES = ['mqtt']
DOMAIN = "arwn"
TOPIC = 'arwn/#'
SENSORS = {}
_LOGGER = logging.getLogger(__name__)
def discover_sensors(topic, payload):
"""Given a topic, dynamically create the right sensor type."""
parts = topic.split('/')
unit = payload.get('units', '')
domain = parts[1]
if domain == "temperature":
name = parts[2]
if unit == "F":
unit = TEMP_FAHRENHEIT
else:
unit = TEMP_CELSIUS
return (ArwnSensor(name, 'temp', unit),)
if domain == "barometer":
return (ArwnSensor("Barometer", 'pressure', unit),)
if domain == "wind":
return (ArwnSensor("Wind Speed", 'speed', unit),
ArwnSensor("Wind Gust", 'gust', unit),
ArwnSensor("Wind Direction", 'direction', '°'))
def _slug(name):
return "sensor.arwn_%s" % slugify(name)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the ARWN platform."""
def sensor_event_received(topic, payload, qos):
"""Process events as sensors.
When a new event on our topic (arwn/#) is received we map it
into a known kind of sensor based on topic name. If we've
never seen this before, we keep this sensor around in a global
cache. If we have seen it before, we update the values of the
existing sensor. Either way, we push an ha state update at the
end for the new event we've seen.
This lets us dynamically incorporate sensors without any
configuration on our side.
"""
event = json.loads(payload)
sensors = discover_sensors(topic, event)
if not sensors:
return
if 'timestamp' in event:
del event['timestamp']
for sensor in sensors:
if sensor.name not in SENSORS:
sensor.hass = hass
sensor.set_event(event)
SENSORS[sensor.name] = sensor
_LOGGER.debug("Registering new sensor %(name)s => %(event)s",
dict(name=sensor.name, event=event))
add_devices((sensor,))
else:
SENSORS[sensor.name].set_event(event)
SENSORS[sensor.name].update_ha_state()
mqtt.subscribe(hass, TOPIC, sensor_event_received, 0)
return True
class ArwnSensor(Entity):
"""Represents an ARWN sensor."""
def __init__(self, name, state_key, units):
"""Initialize the sensor."""
self.hass = None
self.entity_id = _slug(name)
self._name = name
self._state_key = state_key
self.event = {}
self._unit_of_measurement = units
def set_event(self, event):
"""Update the sensor with the most recent event."""
self.event = {}
self.event.update(event)
@property
def state(self):
"""Return the state of the device."""
return self.event.get(self._state_key, None)
@property
def name(self):
"""Get the name of the sensor."""
return self._name
@property
def state_attributes(self):
"""Return all the state attributes."""
return self.event
@property
def unit_of_measurement(self):
"""Unit this state is expressed in."""
return self._unit_of_measurement
@property
def should_poll(self):
"""Should we poll."""
return False

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