Compare commits

...

132 Commits
0.19 ... 0.20.2

Author SHA1 Message Date
Paulus Schoutsen
644d5de890 Merge pull request #2154 from home-assistant/hotfix-20-2
Hotfix 20 2
2016-05-23 22:25:59 -07:00
Robbie Trencheny
343625d539 If we have duration_in_traffic use that as the state, otherwise use duration 2016-05-23 23:43:22 -04:00
Robbie Trencheny
2e10b4bf67 If no departure time is set, use now as the default. If departure time is set but does not have a :, assume its a preformed Unix timestamp and send along as raw input. Assume same for arrival_time. 2016-05-23 23:43:06 -04:00
Jan Harkes
d86a5a1e91 Don't even bother trying to kill stray child processes.
When we change our process group id we don't get keyboard interrupt
signals passed if our parent is a bash script.
2016-05-23 23:29:53 -04:00
Jan Harkes
1327051277 Version bump to 0.20.2 2016-05-23 23:29:53 -04:00
Robbie Trencheny
2544635921 Update issue template to prettify the header. 2016-05-23 13:08:47 -07:00
Paulus Schoutsen
3c364fa7e9 Merge pull request #2132 from home-assistant/hotfix-20-1
Hotfix 0.20.1
2016-05-22 09:11:47 -07:00
Jan Harkes
ceb0ec5fa4 Ignore assertions from python threading when looking for leaked threads.
While looking for leaked resources (threads) after shutdown and before restart
we in some cases get an assertion in the python threading module where we find
a thread marked as running at the python level but it has no associated thread
at the C level.
2016-05-22 00:22:19 -04:00
Jan Harkes
a28196df9a Version bump to 0.20.1 2016-05-22 00:21:19 -04:00
Paulus Schoutsen
37bd93a975 Version bump to 0.20 2016-05-21 14:17:02 -07:00
Paulus Schoutsen
b78765a41f Merge pull request #2113 from home-assistant/dev
0.20
2016-05-21 14:15:42 -07:00
Paulus Schoutsen
ab60b32326 Update frontend 2016-05-21 14:06:07 -07:00
Jan Harkes
3ea179cc0b Let systemd handle home-assistant process restarts. (#2127) 2016-05-21 12:58:14 -07:00
Paulus Schoutsen
5bedf5d604 Upgrade Nest to 2.9.2 (#2126) 2016-05-21 11:57:33 -07:00
Ardi Mehist
d8c1959715 Add support for Logentries (#1945)
* Add support for Logentries

Supports sending has events to Logentries web hook endpoint
see logentries.com for more

Inspired by the Splunk component

* bugfix

* fix summary

* fix test

* fix logentries url and tests

* update tests

* mock token

* Bug fixes

* typo

* typo

* fix string splitting

* remove redundant backslash
2016-05-21 11:21:23 -07:00
Robbie Trencheny
0f1c4d2f8c GTFS fixes (#2119)
* Change to official PyGTFS source

* Threading fixes for GTFS

* Actually pygtfs 0.1.3

* Update requirements_all.txt

* Update gtfs version
2016-05-21 11:04:18 -07:00
Igor Shults
3ce6c732ab #2120 Fix hvac z-wave fan list (#2121)
* #2120 Fix hvac z-wave fan list

* Properly name methods
2016-05-21 10:56:20 -07:00
Nolan Gilley
191fc8f8d4 Change color_RGB_to_xy formula & return brightness (#2095)
* Use RGB to XY calculations from Philips Hue developer site

* uppercase X,Y,Z

* rename cx,cy to x,y

* return brightness in color_RGB_to_xy

* remove try/catch

* update existing platforms using color_RGB_to_xy

* improve wemo w/ jaharkes suggestion

* allow brightness override of rgb_to_xy
2016-05-21 10:19:27 -07:00
Johann Kellerman
31c2d45a7a Updated pyqwikswitch & QS<->HA UI behaviour (#2123)
* Updated pyqwikswitch & constants

* Disable too-many-locals
2016-05-21 10:12:42 -07:00
Dan
dee6355cc5 Onkyo updates (#2084)
* use sane defaults for openzwave config

Use sane default if libopenzwave is installed. In most cases this will
mean that the zwave config path will not need to e manually specified.

* Resuming work on onkyo component

* Source control added to UI for onkyo receiver

Source will now display in the UI. Source mappings can be defined in the
config, and a rudimentary mapping is defined by default as a fallback.
When the onkyo source is updated, it will resolve to a defined name if
possible. This may break existing automations.

* fix lint errors

* Updated Onkyo receiver

Now takes an optional ip/name in additional to atempting to discover
deivces.

Source select will now take a sources mapping in the config. It will
provide default values if no source mapping is provided.

example:

- platform: onkyo
  host: 10.0.0.2
  name: receiver
  sources:
    HTPC: 'pc'
    Chromecast: 'aux1'
    Bluray: 'bd'
    Wii U: 'game'

* fix pylint error

* Use HA's error log instead of stack trace

* Flipped source mappings, code cleanup
2016-05-21 10:04:08 -07:00
Robbie Trencheny
c9b5ea97da Fix docstring issues with MoldIndicator 2016-05-21 10:03:24 -07:00
Felix
eaebe83429 Moldindicator Sensor (#1575)
* Adds MoldIndicator sensor platform

This sensor may be used to get an indication for possible mold growth in rooms.
It calculates the humidity at a pre-calibrated indoor point (wall, window).

* Automatic conversion to Fahrenheit for mold_indicator

* Minor change to critical temp label

* Fixed docstrings and styles

* Minor changes to MoldIndicator implementation

* Added first (non-working) implementation for mold_indicator test

* Small style changes

* Minor improvements to mold_indicator

* Completed unit test for mold indicator

* Fix to moldindicator initialization

* Adds missing period. Now that really matters..

* Adds test for sensor_changed function
2016-05-21 09:58:59 -07:00
Fabian Affolter
7f0b8c5e70 Docs (#2124)
* Add link to docs

* Update link
2016-05-21 16:59:52 +02:00
Jan Harkes
53d51a467d Single process restart fixes (#2118)
* Ignore permission errors on setpgid.

When launched in a docker container we got a permission denied error
from setpgid.

* Don't fail if we find our own pidfile.

When we restart using exec we are running a new instance of home-assistant with
the same process id so we shouldn't be surprised to find an existing pidfile in
that case.

* Allow restart to work when started as python -m homeassistant.

When we are started with `python -m homeassistant`, the restart command line
becomes `python /path/to/hass/homeassistant/__main__.py`. But in that case the
python path includes `/path/to/hass/homeassistant` instead of `/path/to/hass`
and we fail on the first import.

Fix this by recognizing `/__main__.py` as part of the first argument and
injecting the proper path as PYTHONPATH environment before we start the new
home-assistant instance.
2016-05-20 11:45:16 -07:00
Alexander Fortin
7eeb623b8f Add media_player.sonos_group_players service (#2087)
Sonos platform supports a `party mode` feature that groups all
available players into a single group, of which the calling player
will be the coordinator.
2016-05-20 09:54:15 -07:00
Jan Harkes
6b724f7da4 Not sure why, but this fixed a bad filedescriptor error. (#2116) 2016-05-20 07:03:08 -07:00
Alexander Fortin
a4409da700 Add add_uri_to_queue support to (sonos) media player (#1946)
Sonos (SoCo) supports add_uri_to_queue capability, making it possible
to stream media available via HTTP for example. This patch extends
media_player component and sonos platform to support this feature
2016-05-19 23:30:19 -07:00
John Arild Berentsen
1eb3181c14 Fix fitbit KeyError (#2077)
* Fix fitbit KeyError

* Set units compared to temperature_unit

* Pass true or false for is_metric
2016-05-19 23:28:53 -07:00
wokar
f7b401a20e Added the lg_netcast platform to control a LG Smart TV running NetCast 3.0 or 4.0 (#2081)
* Added the `lgtv` platform to control a LG Smart TV running NetCast 3.0
(LG Smart TV models released in 2012) and NetCast 4.0 (LG Smart TV models released in 2013).

* Fixed multi-line docstring closing quotes

* Rename lgtv to lg_netcast

* Rename lgtv to lg_netcast

* Extracted class to control the LG TV into a separate Python package 'pylgnetcast' and changed requirements accordingly.

* regenerated requirements_all.txt with script

* now uses pylgnetcast v0.2.0 which uses the requests package for the communication with the TV

* fixed lint error: Catching too general exception Exception
2016-05-19 23:27:47 -07:00
Jan Harkes
5f92ceeea9 Allow for restart without using parent/child processes. (#1793)
* Allow for restart without using parent/child processes.

Assuming that we normally correctly shut down running threads and
release resources, we just do some minimal scrubbing of open file
descriptors and child processes which would stay around across an
exec() boundary.

* Use sys.executable instead of multiprocessing.spawn.get_executable()

* Limit how many file descriptors we try to close.

Don't even try to close on OSX/Darwin until we figure out how to
recognize guarded fds because the kernel will yell at us, and kill
the process.

* Use the close on exec flag on MacOS to clean up.

* Introduce a small process runner to handle restart on windows.

* Handle missing signal.SIGHUP on Windows.
2016-05-19 23:20:59 -07:00
Per Sandström
f0f1fadee1 redirect daemon file descriptors (#2103) 2016-05-19 23:20:07 -07:00
Paulus Schoutsen
32f97dc578 Merge remote-tracking branch 'origin/master' into dev
Conflicts:
	homeassistant/const.py
2016-05-19 22:32:34 -07:00
Greg Dowling
631ba2ef0d Merge pull request #2110 from home-assistant/bump_loop_energy
Bump loop energy library version.
2016-05-19 17:19:06 +01:00
pavoni
62de16804b Bump loop energy library version. 2016-05-19 17:12:19 +01:00
Paulus Schoutsen
3d919f1235 Merge pull request #2108 from home-assistant/owntracks_fixes
Owntracks fixes
2016-05-19 08:28:36 -07:00
pavoni
8ff9506138 Ignore acc: 0 updates. 2016-05-19 16:16:43 +01:00
pavoni
dd1703469e Handle region enter/leave with spaces. 2016-05-19 16:04:55 +01:00
Daniel Høyer Iversen
5f98a70c21 Fix bug in flaky rfxtrx test (#2107) 2016-05-19 06:36:11 -07:00
Fabian Affolter
bfd64ce96e Upgrade python-telegram-bot to 4.1.1 (#2102) 2016-05-18 17:05:08 -07:00
Fabian Affolter
a032e649f5 Upgrade psutil to 4.2.0 (#2101) 2016-05-18 17:04:59 -07:00
Robbie Trencheny
c96a5d5b2b Fix profile usage with aws notify platforms (#2100) 2016-05-17 16:51:38 -07:00
Robbie Trencheny
a565cc4b73 Catch a gntp networkerror (#2099) 2016-05-17 16:51:32 -07:00
froz
8d34b76d51 Restored telnet as an option. Activate with config option 'protocol: telnet'. Default is ssh (#2096) 2016-05-17 15:55:12 -07:00
happyleavesaoc
15f89fc636 add some include_dir options (#2074)
* add some include_dir options

* validate, and extend instead of add

* add yaml include tests
2016-05-17 15:47:44 -07:00
Robbie Trencheny
a431277de1 Accept human readable color names to change light colors (#2075)
* Add support for providing color_name which accepts a CSS3 valid, human readable string such as red or blue

* Forgot the schema validation!

* ugh farcy

* use html5_parse_legacy_color for more input options

* Add webcolors==1.5 to setup.py

* Block pylint no-member errors on tuple

* add color_name_to_rgb test

* whoops

* revert changes to individual platforms

* If color_name is set, pop it off params and set rgb_color with it

* Forgot to reset wink.py

* Import the legacy function as color_name_to_rgb directly

* reset test_color.py

* Improve light services.yaml
2016-05-17 00:06:55 -07:00
Alexander Fortin
7208ff515e Better handle exceptions from Sonos players (#2085)
Sonos players can be dynamically set in various modes, for example
as TV players or Line-IN or straming from radios channels, therefore
some methods could not be available, and when invoked they cause
long exceptions to be logged. This partially solves the problem
reducing the output and logging some more informative error message
2016-05-16 22:58:57 -07:00
Paulus Schoutsen
0a79a5e964 Update frontend repo 2016-05-16 21:59:39 -07:00
Daniel Høyer Iversen
8e766daa11 Merge pull request #2086 from home-assistant/time_travel_fix
Round minutes to integer in google travel time, Fix issue #2080
2016-05-16 11:45:43 +02:00
Daniel
4ded795740 Round minutes to integer in google travel time, Fix issue #2080 2016-05-16 11:37:17 +02:00
Robbie Trencheny
84cb7a4f20 Add AWS notify platforms (Lambda, SNS, SQS) (#2073)
* AWS SNS notify platform

* Attach kwargs as MessageAttributes

* Initial pass of AWS SQS platform

* Add Lambda notify platform

* Remove unused import

* Change single quotes to double quotes because I am crazy

* Forgot to run pydocstyle

* Improve context support for Lambda

* compress the message_attributes logic
2016-05-15 13:17:35 -07:00
Rowan
cbf0caa88a Last.fm sensor (#2071)
* Last.fm component

* Pylint fixes

* Last.fm component

* Pylint fixes

* Updated with `.coveragerc` and `requirements_all.txt`

* Pylint fixes

* Updated

* Pylint fix

* Pylint fix
2016-05-15 13:11:41 -07:00
Brent
88d13f0ac9 Added support for the roku media player (#2046) 2016-05-15 13:00:31 -07:00
mnestor
3ed6be5b4e add link ability to configurator (#2035) 2016-05-15 12:56:29 -07:00
Richard Cox
0340710e5c Support for Nest Protect smoke alarms (#2076)
* Support for Nest Protect smoke alarms

* Fixing formatting issues from tox
2016-05-15 12:29:12 -07:00
froz
49acdaa8fd Device Tracker - ASUSWRT: Replaced telnet with ssh (#2079) 2016-05-15 12:20:17 -07:00
Alex Harvey
ffbc99fac2 Merge pull request #2059 from infamy/justyns-purge_old_data
Justyns purge old data
2016-05-14 23:55:28 -07:00
Johann Kellerman
6dae005b65 Resolved UI flicker, new config vars, brightness up to 255, fixed buttons, fixed race condition (#2072) 2016-05-14 14:21:05 -07:00
Robbie Trencheny
0adc853741 Add notify.twilio_sms component (#2070) 2016-05-14 14:09:28 -07:00
Robbie Trencheny
6254d4a983 Add lines for associated documentation PR 2016-05-14 14:02:14 -07:00
Robbie Trencheny
a7db208b8a Fix Google Voice documentation URL 2016-05-14 13:32:00 -07:00
Rowan
429bf2c143 Google Play Music Desktop Player component (#1788)
* Added GPM Desktop Plaeyr component

* Updated requirements_all.txt

* Pylint fix

* Updated GPMDP.py to include @balloob's comments

* Updated to work with the latest version of GPMDP

* Removed setting "self._ws.recv()" as a variable

* Made line 52 shorter

* Updated to check weather it is connected or not

* Pylint and @balloob fixes

* Updated with simplified code and pylint fix

* Made `json.loads` shorter

* Pylint fix
2016-05-14 13:28:42 -07:00
happyleavesaoc
8df91e6a17 numeric state: validate multiple entities (#2066)
* validate multiple entities

* point to current entity
2016-05-14 12:29:57 -07:00
Daniel Høyer Iversen
8656bbbc79 fix bugs in google travel time (#2069) 2016-05-14 12:14:13 -07:00
Daniel Høyer Iversen
630b7377bd Refactor get_age in util/dt (#2067) 2016-05-14 12:05:46 -07:00
Daniel Høyer Iversen
0626a80186 Merge pull request #2068 from home-assistant/yaml_env
Add test for yaml enviroment
2016-05-14 20:33:46 +02:00
Daniel
24788b106b Add test for yaml enviroment 2016-05-14 20:20:27 +02:00
Igor Shults
c5401b21c2 Fix typo in system monitor ('recieved') (#2062) 2016-05-14 09:45:32 -07:00
mnestor
954b56475e YAML: add !include_named_dir and ! include_list_dir (#2054)
* add include_dir constructor for yaml parsing

* changed to allow for flat and name based directory including

* fixed ci errors

* changed flat to list
2016-05-13 21:16:04 -07:00
Alex Harvey
53d7e0730c Fixes for farcy 2016-05-13 14:43:22 -07:00
Alex Harvey
cba85cad8d Fixes for farcy 2016-05-13 14:42:08 -07:00
Lewis Juggins
96b73684eb Update Dockerfile to use OpenSSL 1.0.2h to resolve certificate issues (#2057) 2016-05-13 07:55:52 -07:00
Robbie Trencheny
aa7fa7b550 Dont default to driving anymore, re: #2047 2016-05-12 22:49:12 -07:00
Robbie Trencheny
d229cb46b1 Google travel time improvements (#2047)
* Update google_travel_time.py

* Update google_travel_time.py

* pylint: disable=too-many-instance-attributes

* Add the mode to the title of the sensor

* Expose the travel mode on the sensor attributes

* Big improvements to the Google Travel Time sensor. Allow passing any options that Google supports in the options dict of your configuration. Deprecate travel_mode. Change name format to show the mode

* fu farcy

* Dynamically convert departure and arrival times

* Add a warning if user provides both departure and arrival times

* Add deprecation warning for travel_mode outside options and other minor fixes

* Use a copy of options dict to not overwrite the departure/arrival times constantly.

* Remove default travel_mode, but set default options.mode to driving

* Google doesnt let us query time in the past, so if the date we generate from a time string is in the past, add 1 day

* spacing fix

* Add config validation for all possible parameters

* flake8 and pylint fixes
2016-05-12 22:37:08 -07:00
happyleavesaoc
8682e2def8 supervisord sensor (#2056) 2016-05-12 22:16:58 -07:00
Johann Kellerman
65ac1ae84a Added QwikSwitch component & platforms (#1970)
* Added QwikSwitch platform

farcy - worst than my english teacher

* Clean up comments

* Import only inside functions

* Moved imports, no global var, load_platform

* add_device reworked

* Only serializable content on bus

* Fixed imports & removed some logging
2016-05-12 21:39:30 -07:00
Alex Harvey
93fd6fa11b fixes for pep and delay start 2016-05-12 10:33:22 -07:00
Alex Harvey
67b0365f62 update to latest base 2016-05-12 10:32:28 -07:00
Paulus Schoutsen
f1eda430cd Update rpi_rf.py 2016-05-12 00:13:48 -07:00
Paulus Schoutsen
69929f15fb Ignore RPI-RF in requirements_all 2016-05-11 22:56:05 -07:00
Robbie Trencheny
d553c7c8e7 Merge pull request #2027 from robbiet480/relative-time-filter
Add a Jinja filter for relative time
2016-05-11 22:50:56 -07:00
Robbie Trencheny
4d0b9f1e94 Stupid blank lines 2016-05-11 22:44:44 -07:00
Robbie Trencheny
fca4ec2b3e simplify the relative_time function 2016-05-11 22:37:37 -07:00
Robbie Trencheny
b75aa6ac08 Add get_age tests 2016-05-11 22:29:55 -07:00
Nolan Gilley
894ceacd40 Add Ecobee notify platform (#2021)
* add send_message to ecobee via service call

* farcy fixes

* fix pydocstyle

* ecobee notify component
2016-05-11 22:03:21 -07:00
Johann Kellerman
fbe940139a Discovery listener on all EntityComponents (#2042) 2016-05-11 21:58:22 -07:00
happyleavesaoc
c341ae0a39 Media Player - MPD: handle more exceptions (#2045) 2016-05-11 21:53:56 -07:00
Nolan Gilley
f9d97c4356 fix away mode. issue 2032 (#2044) 2016-05-11 21:52:56 -07:00
Nolan Gilley
b8a5d392c5 Fix speedtest by removing Throttle and adding second parameter for track_time_change (#2040) 2016-05-11 08:24:50 -07:00
Paulus Schoutsen
fd8240241f Merge pull request #2038 from home-assistant/hotfix-19-4
Hotfix 0.19.4: Fix script syntax validation of AND and OR condition
2016-05-10 21:57:22 -07:00
Paulus Schoutsen
3c9e493494 Make AND and OR conditions valid (#2037) 2016-05-10 21:49:58 -07:00
Paulus Schoutsen
786a0154b1 Version bump to 0.19.4 2016-05-10 21:48:05 -07:00
Paulus Schoutsen
dd6ab79e35 Make AND and OR conditions valid 2016-05-10 21:47:46 -07:00
Erik Eriksson
2f118c5327 log received mqtt messages (#2031) 2016-05-10 21:12:14 -07:00
Nolan Gilley
a7d1f52ac8 Use Throttle on speedtest update (#2036)
* use throttle

* fix flake8
2016-05-10 20:51:55 -07:00
Robbie Trencheny
5317f700d7 Merge pull request #2033 from home-assistant/hotfix-0193
Hotfix 0193
2016-05-10 13:56:15 -07:00
Robbie Trencheny
01eb2d5c84 Increment version 2016-05-10 13:42:23 -07:00
Paulus Schoutsen
0893ddcab7 Update README.rst 2016-05-10 13:41:23 -07:00
Landrash
e77a7f4385 Fixed minor miss-spelling (#2028)
Changed millileters to milliliters.
Changed case of mmol/l to mmol/L.
2016-05-10 13:40:39 -07:00
Robbie Trencheny
39e7942dce Fitbit flake8 and pylint fixes. Forgot to do it before pushing :( 2016-05-10 13:40:34 -07:00
Robbie Trencheny
faf5ffe610 Minor Fitbit tweaks. Correct the copy, dont require auth on the routes, get the client_id/client_secret from fitbit.conf instead of the YAML 2016-05-10 13:40:25 -07:00
Robbie Trencheny
8d2dc48261 en_UK->en_GB. Closes #2019. 2016-05-10 13:40:17 -07:00
Paulus Schoutsen
c7cfa8d245 Update README.rst 2016-05-10 13:36:03 -07:00
Robbie Trencheny
16933abce9 Remove humanize and use a relative time thing that @balloob found on Github 2016-05-10 00:04:53 -07:00
Landrash
8163b986c9 Fixed minor miss-spelling (#2028)
Changed millileters to milliliters.
Changed case of mmol/l to mmol/L.
2016-05-09 23:48:48 -07:00
Robbie Trencheny
d5a1c52359 Add a Jinja filter for relative time 2016-05-09 23:31:02 -07:00
Nolan Gilley
26ea4e41cb Bring back custom scan intervals and service for speedtest.net component (#1980)
* Bring back the functionality that was removed in PR 1717. This includes the speedtest service and the ability to define the scan times in the configuration file.  Restore default functionality of 1 scan per hour on the hour.

* remove unnecessary code.
2016-05-09 22:49:26 -07:00
Johann Kellerman
ec9544b9c3 Add a load_platform mechanism (#2012)
* discovery.load_platform method

* rm grep
2016-05-09 22:48:03 -07:00
Fabian Affolter
1d0bc1ee66 Upgrade flake8 to 2.5.4 (#2018) 2016-05-09 22:33:21 -07:00
Robbie Trencheny
9729c44d53 Merge pull request #2023 from philipbl/fix_slack
Fix problem with Slack default channel
2016-05-09 16:37:38 -07:00
Robbie Trencheny
a7292af3b1 Fitbit flake8 and pylint fixes. Forgot to do it before pushing :( 2016-05-09 15:33:04 -07:00
Robbie Trencheny
c8cbc528eb Minor Fitbit tweaks. Correct the copy, dont require auth on the routes, get the client_id/client_secret from fitbit.conf instead of the YAML 2016-05-09 15:31:47 -07:00
Philip Lundrigan
8735bfe926 Fix problem with default channel 2016-05-09 16:19:19 -06:00
Robbie Trencheny
25e8c7bc5f en_UK->en_GB. Closes #2019. 2016-05-09 15:14:33 -07:00
jazzaj
499257c8e1 Corrected link to documentation (#2022) 2016-05-09 23:30:22 +02:00
Paulus Schoutsen
6856283896 Make HVAC naming consistent (#2017) 2016-05-09 07:53:01 -07:00
Paulus Schoutsen
20dad9f194 Add HVAC to demo 2016-05-08 23:21:26 -07:00
Paulus Schoutsen
09483e3be4 More fault tolerant discovery 2016-05-08 21:23:03 -07:00
Paulus Schoutsen
8ae5708fa2 Merge pull request #2010 from home-assistant/hotfix-0192
Hotfix 0192
2016-05-08 10:19:54 -07:00
Paulus Schoutsen
1e42f85a9c Version bump to 0.19.2 2016-05-08 09:53:22 -07:00
John Arild Berentsen
92d71a6612 Fix for not recognizing Z-Wave thermostats (#2006)
* Fix for not recognizing thermostats

* Properly ignore zxt-120

* fix
2016-05-08 09:53:09 -07:00
John Arild Berentsen
ab2e85840f Fix for not recognizing Z-Wave thermostats (#2006)
* Fix for not recognizing thermostats

* Properly ignore zxt-120

* fix
2016-05-08 09:52:16 -07:00
Paulus Schoutsen
8257e3f384 Fix automation deprecation warning 2016-05-07 22:31:22 -07:00
Paulus Schoutsen
e40908d67c Improve config validation error message 2016-05-07 22:31:22 -07:00
Paulus Schoutsen
1abbb43ebd Merge pull request #2004 from home-assistant/hotfix-019-1
Hotfix 019 1
2016-05-07 22:31:00 -07:00
Paulus Schoutsen
69ecedefad Version bump to 0.19.1 2016-05-07 22:24:20 -07:00
Paulus Schoutsen
0f6c9d2f75 Fix automation deprecation warning 2016-05-07 22:24:13 -07:00
Paulus Schoutsen
c58fb00f03 Improve config validation error message 2016-05-07 22:24:04 -07:00
Paulus Schoutsen
e67729b2f4 Version bump to 0.20.0.dev0 2016-05-07 12:55:41 -07:00
Justyn Shull
bf3b77e1f2 Change sqlite queries to work with older versions of sqlite 2016-04-15 21:18:51 -05:00
Justyn Shull
d5ca97b1f6 Add tests for purging old states and events 2016-04-15 21:02:17 -05:00
Justyn Shull
fd48fc5f83 Add CONFIG_SCHEMA to verify config. Move purge_days key name to
CONF_PURGE_DAYS so it can be changed easier later.

Use 'recorder' domain instead of 'history' domain.

Pass purge_days config directly into Recorder object instead of passing
the config object around.
2016-04-15 19:54:30 -05:00
Justyn Shull
c89cd6a68c Add 'purge_days' option to the history/recorder component
Issue https://github.com/balloob/home-assistant/issues/1337

When purge_days is set under the history component, recorder.py will
delete all events and states that are older than purge_days days ago.

Currently, this is only done once at start up.   A vacuum command is
also run to free up the disk space sqlite would still use after deleting
records.
2016-04-15 19:54:30 -05:00
95 changed files with 3181 additions and 421 deletions

View File

@@ -38,6 +38,9 @@ omit =
homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py
homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py
homeassistant/components/rpi_gpio.py
homeassistant/components/*/rpi_gpio.py
@@ -111,18 +114,24 @@ omit =
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py
homeassistant/components/notify/googlevoice.py
@@ -138,6 +147,7 @@ omit =
homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twitter.py
homeassistant/components/notify/xmpp.py
homeassistant/components/scene/hunterdouglas_powerview.py
@@ -153,6 +163,7 @@ omit =
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/netatmo.py
homeassistant/components/sensor/neurio_energy.py
@@ -163,6 +174,7 @@ omit =
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py

View File

@@ -1,4 +1,6 @@
Make sure you run the latest version before reporting an issue. Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests
Make sure you are running the latest version of Home Assistant before reporting an issue.
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
**Home Assistant release (`hass --version`):**

View File

@@ -3,6 +3,8 @@
**Related issue (if applicable):** #
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
**Example entry for `configuration.yaml` (if applicable):**
```yaml
@@ -10,6 +12,9 @@
**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)
If code communicates with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).

2
.gitignore vendored
View File

@@ -83,3 +83,5 @@ venv
# vimmy stuff
*.swp
*.swo
ctags.tmp

View File

@@ -21,6 +21,14 @@ RUN script/build_python_openzwave && \
COPY requirements_all.txt requirements_all.txt
RUN pip3 install --no-cache-dir -r requirements_all.txt
RUN wget http://www.openssl.org/source/openssl-1.0.2h.tar.gz && \
tar -xvzf openssl-1.0.2h.tar.gz && \
cd openssl-1.0.2h && \
./config --prefix=/usr/ && \
make && \
make install && \
rm -rf openssl-1.0.2h*
# Copy source
COPY . .

View File

@@ -1,5 +1,5 @@
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs|
==================================================================================================================
==============================================================================================================================================================================================
Home Assistant is a home automation platform running on Python 3. The
goal of Home Assistant is to be able to track and control all devices at

View File

@@ -3,11 +3,11 @@ from __future__ import print_function
import argparse
import os
import signal
import platform
import subprocess
import sys
import threading
import time
from multiprocessing import Process
from homeassistant.const import (
__version__,
@@ -87,8 +87,7 @@ def get_arguments():
parser.add_argument(
'--debug',
action='store_true',
help='Start Home Assistant in debug mode. Runs in single process to '
'enable use of interactive debuggers.')
help='Start Home Assistant in debug mode')
parser.add_argument(
'--open-ui',
action='store_true',
@@ -123,15 +122,20 @@ def get_arguments():
'--restart-osx',
action='store_true',
help='Restarts on OS X.')
if os.name != "nt":
parser.add_argument(
'--runner',
action='store_true',
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
if os.name == "posix":
parser.add_argument(
'--daemon',
action='store_true',
help='Run Home Assistant as daemon')
arguments = parser.parse_args()
if os.name == "nt":
if os.name != "posix" or arguments.debug or arguments.runner:
arguments.daemon = False
return arguments
@@ -144,13 +148,21 @@ def daemonize():
# Decouple fork
os.setsid()
os.umask(0)
# Create second fork
pid = os.fork()
if pid > 0:
sys.exit(0)
# redirect standard file descriptors to devnull
infd = open(os.devnull, 'r')
outfd = open(os.devnull, 'a+')
sys.stdout.flush()
sys.stderr.flush()
os.dup2(infd.fileno(), sys.stdin.fileno())
os.dup2(outfd.fileno(), sys.stdout.fileno())
os.dup2(outfd.fileno(), sys.stderr.fileno())
def check_pid(pid_file):
"""Check that HA is not already running."""
@@ -161,6 +173,10 @@ def check_pid(pid_file):
# PID File does not exist
return
# If we just restarted, we just found our own pidfile.
if pid == os.getpid():
return
try:
os.kill(pid, 0)
except OSError:
@@ -220,29 +236,61 @@ def uninstall_osx():
print("Home Assistant has been uninstalled.")
def setup_and_run_hass(config_dir, args, top_process=False):
"""Setup HASS and run.
def closefds_osx(min_fd, max_fd):
"""Make sure file descriptors get closed when we restart.
Block until stopped. Will assume it is running in a subprocess unless
top_process is set to true.
We cannot call close on guarded fds, and we cannot easily test which fds
are guarded. But we can set the close-on-exec flag on everything we want to
get rid of.
"""
from fcntl import fcntl, F_GETFD, F_SETFD, FD_CLOEXEC
for _fd in range(min_fd, max_fd):
try:
val = fcntl(_fd, F_GETFD)
if not val & FD_CLOEXEC:
fcntl(_fd, F_SETFD, val | FD_CLOEXEC)
except IOError:
pass
def cmdline():
"""Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith('/__main__.py'):
modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir, args):
"""Setup HASS and run."""
from homeassistant import bootstrap
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
args = cmdline() + ['--runner']
while True:
try:
subprocess.check_call(args)
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
sys.exit(exc.returncode)
if args.demo_mode:
config = {
'frontend': {},
'demo': {}
}
hass = bootstrap.from_config_dict(
config, config_dir=config_dir, daemon=args.daemon,
verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days)
config, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
hass = bootstrap.from_config_file(
config_file, daemon=args.daemon, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days)
if hass is None:
return
@@ -256,42 +304,53 @@ def setup_and_run_hass(config_dir, args, top_process=False):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
print('Starting Home-Assistant')
hass.start()
exit_code = int(hass.block_till_stopped())
if not top_process:
sys.exit(exit_code)
return exit_code
def run_hass_process(hass_proc):
"""Run a child hass process. Returns True if it should be restarted."""
requested_stop = threading.Event()
hass_proc.daemon = True
def request_stop(*args):
"""Request hass stop, *args is for signal handler callback."""
requested_stop.set()
hass_proc.terminate()
def try_to_restart():
"""Attempt to clean up state and start a new homeassistant instance."""
# Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind.
sys.stderr.write('Home Assistant attempting to restart.\n')
# Count remaining threads, ideally there should only be one non-daemonized
# thread left (which is us). Nothing we really do with it, but it might be
# useful when debugging shutdown/restart issues.
try:
signal.signal(signal.SIGTERM, request_stop)
nthreads = sum(thread.isAlive() and not thread.isDaemon()
for thread in threading.enumerate())
if nthreads > 1:
sys.stderr.write(
"Found {} non-daemonic threads.\n".format(nthreads))
# Somehow we sometimes seem to trigger an assertion in the python threading
# module. It seems we find threads that have no associated OS level thread
# which are not marked as stopped at the python level.
except AssertionError:
sys.stderr.write("Failed to count non-daemonic threads.\n")
# Try to not leave behind open filedescriptors with the emphasis on try.
try:
max_fd = os.sysconf("SC_OPEN_MAX")
except ValueError:
print('Could not bind to SIGTERM. Are you running in a thread?')
max_fd = 256
hass_proc.start()
try:
hass_proc.join()
except KeyboardInterrupt:
request_stop()
try:
hass_proc.join()
except KeyboardInterrupt:
return False
if platform.system() == 'Darwin':
closefds_osx(3, max_fd)
else:
os.closerange(3, max_fd)
return (not requested_stop.isSet() and
hass_proc.exitcode == RESTART_EXIT_CODE,
hass_proc.exitcode)
# Now launch into a new instance of Home-Assistant. If this fails we
# fall through and exit with error 100 (RESTART_EXIT_CODE) in which case
# systemd will restart us when RestartForceExitStatus=100 is set in the
# systemd.service file.
sys.stderr.write("Restarting Home-Assistant\n")
args = cmdline()
os.execv(args[0], args)
def main():
@@ -325,21 +384,10 @@ def main():
if args.pid_file:
write_pid(args.pid_file)
# Run hass in debug mode if requested
if args.debug:
sys.stderr.write('Running in debug mode. '
'Home Assistant will not be able to restart.\n')
exit_code = setup_and_run_hass(config_dir, args, top_process=True)
if exit_code == RESTART_EXIT_CODE:
sys.stderr.write('Home Assistant requested a '
'restart in debug mode.\n')
return exit_code
exit_code = setup_and_run_hass(config_dir, args)
if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart()
# Run hass as child process. Restart if necessary.
keep_running = True
while keep_running:
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args))
keep_running, exit_code = run_hass_process(hass_proc)
return exit_code

View File

@@ -104,7 +104,7 @@ def _setup_component(hass, domain, config):
try:
config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, domain)
cv.log_exception(_LOGGER, ex, domain, config)
return False
elif hasattr(component, 'PLATFORM_SCHEMA'):
@@ -114,11 +114,11 @@ def _setup_component(hass, domain, config):
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, domain)
cv.log_exception(_LOGGER, ex, domain, p_config)
return False
# Not all platform components follow same pattern for platforms
# Sof if p_name is None we are not going to validate platform
# So if p_name is None we are not going to validate platform
# (the automation component is one of them)
if p_name is None:
platforms.append(p_validated)
@@ -136,7 +136,7 @@ def _setup_component(hass, domain, config):
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, '{}.{}'
.format(domain, p_name))
.format(domain, p_name), p_validated)
return False
platforms.append(p_validated)
@@ -215,7 +215,7 @@ def mount_local_lib_path(config_dir):
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
verbose=False, daemon=False, skip_pip=False,
verbose=False, skip_pip=False,
log_rotate_days=None):
"""Try to configure Home Assistant from a config dict.
@@ -228,17 +228,19 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
core_config = config.get(core.DOMAIN, {})
try:
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA(
config.get(core.DOMAIN, {})))
core_config))
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, 'homeassistant')
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
return None
process_ha_config_upgrade(hass)
if enable_log:
enable_logging(hass, verbose, daemon, log_rotate_days)
enable_logging(hass, verbose, log_rotate_days)
hass.config.skip_pip = skip_pip
if skip_pip:
@@ -276,8 +278,8 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
return hass
def from_config_file(config_path, hass=None, verbose=False, daemon=False,
skip_pip=True, log_rotate_days=None):
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
log_rotate_days=None):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@@ -291,7 +293,7 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
enable_logging(hass, verbose, daemon, log_rotate_days)
enable_logging(hass, verbose, log_rotate_days)
try:
config_dict = config_util.load_yaml_config_file(config_path)
@@ -302,28 +304,27 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
skip_pip=skip_pip)
def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
def enable_logging(hass, verbose=False, log_rotate_days=None):
"""Setup the logging."""
if not daemon:
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)s")
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
fmt,
datefmt='%y-%m-%d %H:%M:%S',
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)s")
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
fmt,
datefmt='%y-%m-%d %H:%M:%S',
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
pass
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path(ERROR_LOG_FILENAME)

View File

@@ -143,7 +143,7 @@ def _process_if(hass, config, p_config, action):
# Deprecated since 0.19 - 5/5/2016
if cond_type != DEFAULT_CONDITION_TYPE:
_LOGGER.warning('Using condition_type: %s is deprecated. Please use '
_LOGGER.warning('Using condition_type: "or" is deprecated. Please use '
'"condition: or" instead.')
if_configs = p_config.get(CONF_CONDITION)

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers import condition, config_validation as cv
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'numeric_state',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
CONF_BELOW: vol.Coerce(float),
CONF_ABOVE: vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
@@ -41,7 +41,7 @@ def trigger(hass, config, action):
variables = {
'trigger': {
'platform': 'numeric_state',
'entity_id': entity_id,
'entity_id': entity,
'below': below,
'above': above,
}

View File

@@ -19,6 +19,8 @@ SERVICE_CONFIGURE = "configure"
STATE_CONFIGURE = "configure"
STATE_CONFIGURED = "configured"
ATTR_LINK_NAME = "link_name"
ATTR_LINK_URL = "link_url"
ATTR_CONFIGURE_ID = "configure_id"
ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_IMAGE = "description_image"
@@ -34,7 +36,7 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-arguments
def request_config(
hass, name, callback, description=None, description_image=None,
submit_caption=None, fields=None):
submit_caption=None, fields=None, link_name=None, link_url=None):
"""Create a new request for configuration.
Will return an ID to be used for sequent calls.
@@ -43,7 +45,8 @@ def request_config(
request_id = instance.request_config(
name, callback,
description, description_image, submit_caption, fields)
description, description_image, submit_caption,
fields, link_name, link_url)
_REQUESTS[request_id] = instance
@@ -100,7 +103,8 @@ class Configurator(object):
# pylint: disable=too-many-arguments
def request_config(
self, name, callback,
description, description_image, submit_caption, fields):
description, description_image, submit_caption,
fields, link_name, link_url):
"""Setup a request for configuration."""
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
@@ -121,6 +125,8 @@ class Configurator(object):
(ATTR_DESCRIPTION, description),
(ATTR_DESCRIPTION_IMAGE, description_image),
(ATTR_SUBMIT_CAPTION, submit_caption),
(ATTR_LINK_NAME, link_name),
(ATTR_LINK_URL, link_url),
] if value is not None
})

View File

@@ -21,6 +21,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
'camera',
'device_tracker',
'garage_door',
'hvac',
'light',
'lock',
'media_player',

View File

@@ -19,13 +19,16 @@ from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1']
_LEASES_CMD = 'cat /var/lib/misc/dnsmasq.leases'
_LEASES_REGEX = re.compile(
r'\w+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'(?P<host>([^\s]+))')
_IP_NEIGH_CMD = 'ip neigh'
_IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'\w+\s' +
@@ -55,6 +58,7 @@ class AsusWrtDeviceScanner(object):
self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME])
self.password = str(config[CONF_PASSWORD])
self.protocol = config.get('protocol')
self.lock = threading.Lock()
@@ -100,8 +104,26 @@ class AsusWrtDeviceScanner(object):
self.last_results = active_clients
return True
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result."""
def ssh_connection(self):
"""Retrieve data from ASUSWRT via the ssh protocol."""
from pexpect import pxssh
try:
ssh = pxssh.pxssh()
ssh.login(self.host, self.username, self.password)
ssh.sendline(_IP_NEIGH_CMD)
ssh.prompt()
neighbors = ssh.before.split(b'\n')[1:-1]
ssh.sendline(_LEASES_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
ssh.logout()
return (neighbors, leases_result)
except pxssh.ExceptionPxssh as exc:
_LOGGER.exception('Unexpected response from router: %s', exc)
return ('', '')
def telnet_connection(self):
"""Retrieve data from ASUSWRT via the telnet protocol."""
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'login: ')
@@ -109,18 +131,26 @@ class AsusWrtDeviceScanner(object):
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
telnet.write('ip neigh\n'.encode('ascii'))
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('cat /var/lib/misc/dnsmasq.leases\n'.encode('ascii'))
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
return (neighbors, leases_result)
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
return ('', '')
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
_LOGGER.exception("Connection refused by router,"
" is telnet enabled?")
return
return ('', '')
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result."""
if self.protocol == 'telnet':
neighbors, leases_result = self.telnet_connection()
else:
neighbors, leases_result = self.ssh_connection()
devices = {}
for lease in leases_result:

View File

@@ -11,7 +11,7 @@ from collections import defaultdict
import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME
from homeassistant.util import convert
from homeassistant.util import convert, slugify
DEPENDENCIES = ['mqtt']
@@ -53,6 +53,12 @@ def setup_scanner(hass, config, see):
'accuracy %s is not met: %s',
data_type, max_gps_accuracy, data)
return None
if convert(data.get('acc'), float, 1.0) == 0.0:
_LOGGER.debug('Skipping %s update because GPS accuracy'
'is zero',
data_type)
return None
return data
def owntracks_location_update(topic, payload, qos):
@@ -91,7 +97,7 @@ def setup_scanner(hass, config, see):
return
# OwnTracks uses - at the start of a beacon zone
# to switch on 'hold mode' - ignore this
location = data['desc'].lstrip("-")
location = slugify(data['desc'].lstrip("-"))
if location.lower() == 'home':
location = STATE_HOME

View File

@@ -15,10 +15,12 @@ from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED)
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.6']
REQUIREMENTS = ['netdisco==0.6.7']
SCAN_INTERVAL = 300 # seconds
LOAD_PLATFORM = 'load_platform'
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast'
@@ -27,6 +29,7 @@ SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver'
SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
SERVICE_ROKU = 'roku'
SERVICE_HANDLERS = {
SERVICE_WEMO: "wemo",
@@ -37,6 +40,7 @@ SERVICE_HANDLERS = {
SERVICE_PLEX: 'media_player',
SERVICE_SQUEEZEBOX: 'media_player',
SERVICE_PANASONIC_VIERA: 'media_player',
SERVICE_ROKU: 'media_player',
}
@@ -52,7 +56,7 @@ def listen(hass, service, callback):
def discovery_event_listener(event):
"""Listen for discovery events."""
if event.data[ATTR_SERVICE] in service:
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
@@ -73,6 +77,32 @@ def discover(hass, service, discovered=None, component=None, hass_config=None):
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
def load_platform(hass, component, platform, info=None, hass_config=None):
"""Helper method for generic platform loading.
This method allows a platform to be loaded dynamically without it being
known at runtime (in the DISCOVERY_PLATFORMS list of the component).
Advantages of using this method:
- Any component & platforms combination can be dynamically added
- A component (i.e. light) does not have to import every component
that can dynamically add a platform (e.g. wemo, wink, insteon_hub)
- Custom user components can take advantage of discovery/loading
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
fired to load the platform. The event will contain:
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
ATTR_DISCOVERED = {LOAD_PLATFORM: <<platform>>} }
* dev note: This listener can be found in entity_component.py
"""
if info is None:
info = {LOAD_PLATFORM: platform}
else:
info[LOAD_PLATFORM] = platform
discover(hass, LOAD_PLATFORM + '.' + component, info, component,
hass_config)
def setup(hass, config):
"""Start a discovery service."""
logger = logging.getLogger(__name__)

View File

@@ -22,7 +22,7 @@ HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/'
'92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4']
'4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5']
_LOGGER = logging.getLogger(__name__)

View File

@@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
VERSION = "77c51c270b0241ce7ba0d1df2d254d6f"
VERSION = "0a226e905af198b2dabf1ce154844568"

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={i:r,l:!1,exports:{}};return e[r].call(s.exports,s,s.exports,t),s.l=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(t.s=192)}({192:function(e,t,n){var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}});
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={i:r,l:!1,exports:{}};return e[r].call(s.exports,s,s.exports,t),s.l=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(t.s=194)}({194:function(e,t,n){var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}});

View File

@@ -29,7 +29,7 @@ SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_OPERATION_MODE = "set_operation_mode"
SERVICE_SET_SWING = "set_swing_mode"
SERVICE_SET_SWING_MODE = "set_swing_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
STATE_HEAT = "heat"
@@ -40,17 +40,17 @@ STATE_DRY = "dry"
STATE_FAN_ONLY = "fan_only"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_AWAY_MODE = "away_mode"
ATTR_AUX_HEAT = "aux_heat"
ATTR_FAN = "fan"
ATTR_FAN_LIST = "fan_list"
ATTR_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_temp"
ATTR_AWAY_MODE = "away_mode"
ATTR_AUX_HEAT = "aux_heat"
ATTR_FAN_MODE = "fan_mode"
ATTR_FAN_LIST = "fan_list"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_OPERATION = "operation_mode"
ATTR_OPERATION_MODE = "operation_mode"
ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list"
@@ -108,7 +108,7 @@ def set_humidity(hass, humidity, entity_id=None):
def set_fan_mode(hass, fan, entity_id=None):
"""Turn all or specified hvac fan mode on."""
data = {ATTR_FAN: fan}
data = {ATTR_FAN_MODE: fan}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
@@ -118,7 +118,7 @@ def set_fan_mode(hass, fan, entity_id=None):
def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode."""
data = {ATTR_OPERATION: operation_mode}
data = {ATTR_OPERATION_MODE: operation_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
@@ -133,7 +133,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_SWING, data)
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
# pylint: disable=too-many-branches
@@ -247,12 +247,12 @@ def setup(hass, config):
"""Set fan mode on target hvacs."""
target_hvacs = component.extract_from_service(service)
fan = service.data.get(ATTR_FAN)
fan = service.data.get(ATTR_FAN_MODE)
if fan is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_FAN_MODE, ATTR_FAN)
SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
return
for hvac in target_hvacs:
@@ -269,16 +269,16 @@ def setup(hass, config):
"""Set operating mode on the target hvacs."""
target_hvacs = component.extract_from_service(service)
operation_mode = service.data.get(ATTR_OPERATION)
operation_mode = service.data.get(ATTR_OPERATION_MODE)
if operation_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION)
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
return
for hvac in target_hvacs:
hvac.set_operation(operation_mode)
hvac.set_operation_mode(operation_mode)
if hvac.should_poll:
hvac.update_ha_state(True)
@@ -296,18 +296,18 @@ def setup(hass, config):
if swing_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_SWING, ATTR_SWING_MODE)
SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
return
for hvac in target_hvacs:
hvac.set_swing(swing_mode)
hvac.set_swing_mode(swing_mode)
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_SWING, swing_set_service,
descriptions.get(SERVICE_SET_SWING))
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
descriptions.get(SERVICE_SET_SWING_MODE))
return True
@@ -330,19 +330,30 @@ class HvacDevice(Entity):
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE:
self._convert_for_display(self.target_temperature),
ATTR_HUMIDITY: self.target_humidity,
ATTR_CURRENT_HUMIDITY: self.current_humidity,
ATTR_MIN_HUMIDITY: self.min_humidity,
ATTR_MAX_HUMIDITY: self.max_humidity,
ATTR_FAN_LIST: self.fan_list,
ATTR_OPERATION_LIST: self.operation_list,
ATTR_SWING_LIST: self.swing_list,
ATTR_OPERATION: self.current_operation,
ATTR_FAN: self.current_fan_mode,
ATTR_SWING_MODE: self.current_swing_mode,
}
humidity = self.target_humidity
if humidity is not None:
data[ATTR_HUMIDITY] = humidity
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
data[ATTR_MIN_HUMIDITY] = self.min_humidity
data[ATTR_MAX_HUMIDITY] = self.max_humidity
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
operation_mode = self.current_operation
if operation_mode is not None:
data[ATTR_OPERATION_MODE] = operation_mode
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
is_away = self.is_away_mode_on
if is_away is not None:
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
@@ -430,11 +441,11 @@ class HvacDevice(Entity):
"""Set new target fan mode."""
pass
def set_operation(self, operation_mode):
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
pass
def set_swing(self, swing_mode):
def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
pass
@@ -457,12 +468,12 @@ class HvacDevice(Entity):
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._convert_for_display(7)
return convert(7, TEMP_CELCIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._convert_for_display(35)
return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
@property
def min_humidity(self):

View File

@@ -118,7 +118,7 @@ class DemoHvac(HvacDevice):
self._target_humidity = humidity
self.update_ha_state()
def set_swing(self, swing_mode):
def set_swing_mode(self, swing_mode):
"""Set new target temperature."""
self._current_swing_mode = swing_mode
self.update_ha_state()
@@ -128,7 +128,7 @@ class DemoHvac(HvacDevice):
self._current_fan_mode = fan
self.update_ha_state()
def set_operation(self, operation_mode):
def set_operation_mode(self, operation_mode):
"""Set new target temperature."""
self._current_operation = operation_mode
self.update_ha_state()

45
homeassistant/components/hvac/zwave.py Normal file → Executable file
View File

@@ -1,5 +1,9 @@
"""ZWave Hvac device."""
"""
Support for ZWave HVAC devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/hvac.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
@@ -19,6 +23,12 @@ REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
COMMAND_CLASS_CONFIGURATION = 0x70
WORKAROUND_ZXT_120 = 'zxt_120'
DEVICE_MAPPINGS = {
@@ -96,22 +106,24 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Set point
for value in self._node.get_values(class_id=0x43).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if int(value.data) != 0:
self._target_temperature = int(value.data)
# Operation Mode
for value in self._node.get_values(class_id=0x40).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
self._current_operation = value.data
self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list)
# Current Temp
for value in self._node.get_values(class_id=0x31).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
self._current_temperature = int(value.data)
self._unit = value.units
# Fan Mode
fan_class_id = 0x44 if self._zxt_120 else 0x42
_LOGGER.debug("fan_class_id=%s", fan_class_id)
for value in self._node.get_values(class_id=fan_class_id).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
self._current_operation_state = value.data
self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list)
@@ -119,7 +131,8 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
self._current_operation_state)
# Swing mode
if self._zxt_120 == 1:
for value in self._node.get_values(class_id=0x70).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data
self._swing_list = [0, 1]
@@ -184,7 +197,8 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_temperature(self, temperature):
"""Set new target temperature."""
for value in self._node.get_values(class_id=0x43).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if value.command_class != 67:
continue
if self._zxt_120:
@@ -200,20 +214,23 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_fan_mode(self, fan):
"""Set new target fan mode."""
for value in self._node.get_values(class_id=0x44).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
if value.command_class == 68 and value.index == 0:
value.data = bytes(fan, 'utf-8')
def set_operation(self, operation_mode):
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
for value in self._node.get_values(class_id=0x40).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
if value.command_class == 64 and value.index == 0:
value.data = bytes(operation_mode, 'utf-8')
def set_swing(self, swing_mode):
def set_swing_mode(self, swing_mode):
"""Set new target swing mode."""
if self._zxt_120 == 1:
for value in self._node.get_values(class_id=0x70).values():
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
value.data = int(swing_mode)

View File

@@ -39,6 +39,7 @@ ATTR_TRANSITION = "transition"
ATTR_RGB_COLOR = "rgb_color"
ATTR_XY_COLOR = "xy_color"
ATTR_COLOR_TEMP = "color_temp"
ATTR_COLOR_NAME = "color_name"
# int with value 0 .. 255 representing brightness of the light.
ATTR_BRIGHTNESS = "brightness"
@@ -87,6 +88,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_PROFILE: str,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: cv.byte,
ATTR_COLOR_NAME: str,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
@@ -122,7 +124,7 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None, profile=None,
flash=None, effect=None):
flash=None, effect=None, color_name=None):
"""Turn all or specified light on."""
data = {
key: value for key, value in [
@@ -135,6 +137,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_COLOR_TEMP, color_temp),
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
(ATTR_COLOR_NAME, color_name),
] if value is not None
}
@@ -228,6 +231,11 @@ def setup(hass, config):
params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
for light in target_lights:
light.turn_on(**params)

View File

@@ -235,14 +235,16 @@ class HueLight(Light):
if ATTR_TRANSITION in kwargs:
command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10
if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_XY_COLOR in kwargs:
command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs:
command['xy'] = color_util.color_RGB_to_xy(
xyb = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['xy'] = xyb[0], xyb[1]
command['bri'] = xyb[2]
if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_COLOR_TEMP in kwargs:
command['ct'] = kwargs[ATTR_COLOR_TEMP]

View File

@@ -0,0 +1,35 @@
"""
Support for Qwikswitch Relays and Dimmers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/
"""
import logging
import homeassistant.components.qwikswitch as qwikswitch
from homeassistant.components.light import Light
DEPENDENCIES = ['qwikswitch']
class QSLight(qwikswitch.QSToggleEntity, Light):
"""Light based on a Qwikswitch relay/dimmer module."""
pass
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Store add_devices for the light components."""
if discovery_info is None or 'qsusb_id' not in discovery_info:
logging.getLogger(__name__).error(
'Configure main Qwikswitch component')
return False
qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']]
for item in qsusb.ha_devices:
if item['type'] not in ['dim', 'rel']:
continue
dev = QSLight(item, qsusb)
add_devices([dev])
qsusb.ha_objects[item['id']] = dev

View File

@@ -16,6 +16,10 @@ turn_on:
description: Color for the light in RGB-format
example: '[255, 100, 100]'
color_name:
description: A human readable color name
example: 'red'
xy_color:
description: Color for the light in XY-format
example: '[0.52, 0.43]'

View File

@@ -105,6 +105,7 @@ class WemoLight(Light):
elif ATTR_RGB_COLOR in kwargs:
xycolor = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
kwargs.setdefault(ATTR_BRIGHTNESS, xycolor[2])
else:
xycolor = None

View File

@@ -97,7 +97,9 @@ class WinkLight(Light):
}
if rgb_color:
state_kwargs['color_xy'] = color_util.color_RGB_to_xy(*rgb_color)
xyb = color_util.color_RGB_to_xy(*rgb_color)
state_kwargs['color_xy'] = xyb[0], xyb[1]
state_kwargs['brightness'] = xyb[2]
if color_temp_mired:
state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired)

View File

@@ -0,0 +1,61 @@
"""
Support for sending data to Logentries webhook endpoint.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/logentries/
"""
import json
import logging
import requests
import homeassistant.util as util
from homeassistant.const import EVENT_STATE_CHANGED
from homeassistant.helpers import state as state_helper
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
DOMAIN = "logentries"
DEPENDENCIES = []
DEFAULT_HOST = 'https://webhook.logentries.com/noformat/logs/'
CONF_TOKEN = 'token'
def setup(hass, config):
"""Setup the Logentries component."""
if not validate_config(config, {DOMAIN: ['token']}, _LOGGER):
_LOGGER.error("Logentries token not present")
return False
conf = config[DOMAIN]
token = util.convert(conf.get(CONF_TOKEN), str)
le_wh = DEFAULT_HOST + token
def logentries_event_listener(event):
"""Listen for new messages on the bus and sends them to Logentries."""
state = event.data.get('new_state')
if state is None:
return
try:
_state = state_helper.state_as_number(state)
except ValueError:
_state = state.state
json_body = [
{
'domain': state.domain,
'entity_id': state.object_id,
'attributes': dict(state.attributes),
'time': str(event.time_fired),
'value': _state,
}
]
try:
payload = {"host": le_wh,
"event": json_body}
requests.post(le_wh, data=json.dumps(payload), timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.exception('Error sending to Logentries: %s', error)
hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener)
return True

View File

@@ -36,6 +36,7 @@ DISCOVERY_PLATFORMS = {
discovery.SERVICE_PLEX: 'plex',
discovery.SERVICE_SQUEEZEBOX: 'squeezebox',
discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera',
discovery.SERVICE_ROKU: 'roku',
}
SERVICE_PLAY_MEDIA = 'play_media'
@@ -62,6 +63,7 @@ ATTR_APP_NAME = 'app_name'
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
ATTR_INPUT_SOURCE = 'source'
ATTR_INPUT_SOURCE_LIST = 'source_list'
ATTR_MEDIA_ENQUEUE = 'enqueue'
MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow'
@@ -144,6 +146,7 @@ MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
ATTR_MEDIA_ENQUEUE: cv.boolean,
})
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
@@ -255,7 +258,7 @@ def media_seek(hass, position, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data)
def play_media(hass, media_type, media_id, entity_id=None):
def play_media(hass, media_type, media_id, entity_id=None, enqueue=None):
"""Send the media player the command for playing media."""
data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
ATTR_MEDIA_CONTENT_ID: media_id}
@@ -263,6 +266,9 @@ def play_media(hass, media_type, media_id, entity_id=None):
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
if enqueue:
data[ATTR_MEDIA_ENQUEUE] = enqueue
hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data)
@@ -363,9 +369,14 @@ def setup(hass, config):
"""Play specified media_id on the media player."""
media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE)
media_id = service.data.get(ATTR_MEDIA_CONTENT_ID)
enqueue = service.data.get(ATTR_MEDIA_ENQUEUE)
kwargs = {
ATTR_MEDIA_ENQUEUE: enqueue,
}
for player in component.extract_from_service(service):
player.play_media(media_type, media_id)
player.play_media(media_type, media_id, **kwargs)
if player.should_poll:
player.update_ha_state(True)

View File

@@ -253,7 +253,7 @@ class CastDevice(MediaPlayerDevice):
"""Seek the media to a specific location."""
self.cast.media_controller.seek(position)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL."""
self.cast.media_controller.play_media(media_id, media_type)

View File

@@ -152,7 +152,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
"""Flag of media commands that are supported."""
return YOUTUBE_PLAYER_SUPPORT
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
self.youtube_id = media_id
self.update_ha_state()

View File

@@ -0,0 +1,158 @@
"""
Support for Google Play Music Desktop Player.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.gpmdp/
"""
import logging
import json
import socket
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_PAUSE, MediaPlayerDevice)
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['websocket-client==0.35.0']
SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the GPMDP platform."""
from websocket import create_connection
name = config.get("name", "GPM Desktop Player")
address = config.get("address")
if address is None:
_LOGGER.error("Missing address in config")
return False
add_devices([GPMDP(name, address, create_connection)])
class GPMDP(MediaPlayerDevice):
"""Representation of a GPMDP."""
# pylint: disable=too-many-public-methods, abstract-method
# pylint: disable=too-many-instance-attributes
def __init__(self, name, address, create_connection):
"""Initialize the media player."""
self._connection = create_connection
self._address = address
self._name = name
self._status = STATE_OFF
self._ws = None
self._title = None
self._artist = None
self._albumart = None
self.update()
def get_ws(self):
"""Check if the websocket is setup and connected."""
if self._ws is None:
try:
self._ws = self._connection(("ws://" + self._address +
":5672"), timeout=1)
except (socket.timeout, ConnectionRefusedError,
ConnectionResetError):
self._ws = None
elif self._ws.connected is True:
self._ws.close()
try:
self._ws = self._connection(("ws://" + self._address +
":5672"), timeout=1)
except (socket.timeout, ConnectionRefusedError,
ConnectionResetError):
self._ws = None
return self._ws
def update(self):
"""Get the latest details from the player."""
websocket = self.get_ws()
if websocket is None:
self._status = STATE_OFF
return
else:
state = websocket.recv()
state = ((json.loads(state))['payload'])
if state is True:
websocket.recv()
websocket.recv()
song = websocket.recv()
song = json.loads(song)
self._title = (song['payload']['title'])
self._artist = (song['payload']['artist'])
self._albumart = (song['payload']['albumArt'])
self._status = STATE_PLAYING
elif state is False:
self._status = STATE_PAUSED
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def state(self):
"""Return the state of the device."""
return self._status
@property
def media_title(self):
"""Title of current playing media."""
return self._title
@property
def media_artist(self):
"""Artist of current playing media (Music track only)."""
return self._artist
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._albumart
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_GPMDP
def media_next_track(self):
"""Send media_next command to media player."""
websocket = self.get_ws()
if websocket is None:
return
websocket.send('{"namespace": "playback", "method": "forward"}')
def media_previous_track(self):
"""Send media_previous command to media player."""
websocket = self.get_ws()
if websocket is None:
return
websocket.send('{"namespace": "playback", "method": "rewind"}')
def media_play(self):
"""Send media_play command to media player."""
websocket = self.get_ws()
if websocket is None:
return
websocket.send('{"namespace": "playback", "method": "playPause"}')
self._status = STATE_PAUSED
self.update_ha_state()
def media_pause(self):
"""Send media_pause command to media player."""
websocket = self.get_ws()
if websocket is None:
return
websocket.send('{"namespace": "playback", "method": "playPause"}')
self._status = STATE_PAUSED
self.update_ha_state()

View File

@@ -320,7 +320,7 @@ class ItunesDevice(MediaPlayerDevice):
response = self.client.previous()
self.update_state(response)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
if media_type == MEDIA_TYPE_PLAYLIST:
response = self.client.play_playlist(media_id)

View File

@@ -278,6 +278,6 @@ class KodiDevice(MediaPlayerDevice):
self.update_ha_state()
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
self._server.Player.Open({media_type: media_id}, {})

View File

@@ -0,0 +1,210 @@
"""
Support for LG TV running on NetCast 3 or 4.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.lg_netcast/
"""
from datetime import timedelta
import logging
from requests import RequestException
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_SELECT_SOURCE, MEDIA_TYPE_CHANNEL, MediaPlayerDevice)
from homeassistant.const import (
CONF_PLATFORM, CONF_HOST, CONF_NAME, CONF_ACCESS_TOKEN,
STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
import homeassistant.util as util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/wokar/pylgnetcast/archive/'
'v0.2.0.zip#pylgnetcast==0.2.0']
SUPPORT_LGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
DEFAULT_NAME = 'LG TV Remote'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "lg_netcast",
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)),
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the LG TV platform."""
from pylgnetcast import LgNetCastClient
client = LgNetCastClient(config[CONF_HOST], config[CONF_ACCESS_TOKEN])
add_devices([LgTVDevice(client, config[CONF_NAME])])
# pylint: disable=too-many-public-methods, abstract-method
# pylint: disable=too-many-instance-attributes
class LgTVDevice(MediaPlayerDevice):
"""Representation of a LG TV."""
def __init__(self, client, name):
"""Initialize the LG TV device."""
self._client = client
self._name = name
self._muted = False
# Assume that the TV is in Play mode
self._playing = True
self._volume = 0
self._channel_name = ''
self._program_name = ''
self._state = STATE_UNKNOWN
self._sources = {}
self._source_names = []
self.update()
def send_command(self, command):
"""Send remote control commands to the TV."""
from pylgnetcast import LgNetCastError
try:
with self._client as client:
client.send_command(command)
except (LgNetCastError, RequestException):
self._state = STATE_OFF
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
"""Retrieve the latest data from the LG TV."""
from pylgnetcast import LgNetCastError
try:
with self._client as client:
self._state = STATE_PLAYING
volume_info = client.query_data('volume_info')
if volume_info:
volume_info = volume_info[0]
self._volume = float(volume_info.find('level').text)
self._muted = volume_info.find('mute').text == 'true'
channel_info = client.query_data('cur_channel')
if channel_info:
channel_info = channel_info[0]
self._channel_name = channel_info.find('chname').text
self._program_name = channel_info.find('progName').text
channel_list = client.query_data('channel_list')
if channel_list:
channel_names = [str(c.find('chname').text) for
c in channel_list]
self._sources = dict(zip(channel_names, channel_list))
# sort source names by the major channel number
source_tuples = [(k, self._sources[k].find('major').text)
for k in self._sources.keys()]
sorted_sources = sorted(
source_tuples, key=lambda channel: int(channel[1]))
self._source_names = [n for n, k in sorted_sources]
except (LgNetCastError, RequestException):
self._state = STATE_OFF
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume / 100.0
@property
def source(self):
"""Return the current input source."""
return self._channel_name
@property
def source_list(self):
"""List of available input sources."""
return self._source_names
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_CHANNEL
@property
def media_channel(self):
"""Channel currently playing."""
return self._channel_name
@property
def media_title(self):
"""Title of current playing media."""
return self._program_name
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_LGTV
def turn_off(self):
"""Turn off media player."""
self.send_command(1)
def volume_up(self):
"""Volume up the media player."""
self.send_command(24)
def volume_down(self):
"""Volume down media player."""
self.send_command(25)
def mute_volume(self, mute):
"""Send mute command."""
self.send_command(26)
def select_source(self, source):
"""Select input source."""
self._client.change_channel(self._sources[source])
def media_play_pause(self):
"""Simulate play pause media player."""
if self._playing:
self.media_pause()
else:
self.media_play()
def media_play(self):
"""Send play command."""
self._playing = True
self._state = STATE_PLAYING
self.send_command(33)
def media_pause(self):
"""Send media pause command to media player."""
self._playing = False
self._state = STATE_PAUSED
self.send_command(34)
def media_next_track(self):
"""Send next track command."""
self.send_command(36)
def media_previous_track(self):
"""Send the previous track command."""
self.send_command(37)

View File

@@ -89,7 +89,13 @@ class MpdDevice(MediaPlayerDevice):
try:
self.status = self.client.status()
self.currentsong = self.client.currentsong()
except mpd.ConnectionError:
except (mpd.ConnectionError, BrokenPipeError, ValueError):
# Cleanly disconnect in case connection is not in valid state
try:
self.client.disconnect()
except mpd.ConnectionError:
pass
self.client.connect(self.server, self.port)
if self.password is not None:
@@ -206,7 +212,7 @@ class MpdDevice(MediaPlayerDevice):
"""Service to send the MPD the command for previous track."""
self.client.previous()
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the media player the command for playing a playlist."""
_LOGGER.info(str.format("Playing playlist: {0}", media_id))
if media_type == MEDIA_TYPE_PLAYLIST:

View File

@@ -9,7 +9,7 @@ import logging
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME
REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/'
'python3.zip#onkyo-eiscp==0.9.2']
@@ -17,29 +17,59 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
KNOWN_HOSTS = []
DEFAULT_SOURCES = {"tv": "TV", "bd": "Bluray", "game": "Game", "aux1": "Aux1",
"video1": "Video 1", "video2": "Video 2",
"video3": "Video 3", "video4": "Video 4",
"video5": "Video 5", "video6": "Video 6",
"video7": "Video 7"}
CONFIG_SOURCE_LIST = "sources"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Onkyo platform."""
import eiscp
from eiscp import eISCP
add_devices(OnkyoDevice(receiver)
for receiver in eISCP.discover())
hosts = []
if CONF_HOST in config and config[CONF_HOST] not in KNOWN_HOSTS:
try:
hosts.append(OnkyoDevice(eiscp.eISCP(config[CONF_HOST]),
config.get(CONFIG_SOURCE_LIST,
DEFAULT_SOURCES),
name=config[CONF_NAME]))
KNOWN_HOSTS.append(config[CONF_HOST])
except OSError:
_LOGGER.error('Unable to connect to receiver at %s.',
config[CONF_HOST])
else:
for receiver in eISCP.discover():
if receiver.host not in KNOWN_HOSTS:
hosts.append(OnkyoDevice(receiver,
config.get(CONFIG_SOURCE_LIST,
DEFAULT_SOURCES)))
KNOWN_HOSTS.append(receiver.host)
add_devices(hosts)
# pylint: disable=too-many-instance-attributes
class OnkyoDevice(MediaPlayerDevice):
"""Representation of a Onkyo device."""
# pylint: disable=too-many-public-methods, abstract-method
def __init__(self, receiver):
def __init__(self, receiver, sources, name=None):
"""Initialize the Onkyo Receiver."""
self._receiver = receiver
self._muted = False
self._volume = 0
self._pwstate = STATE_OFF
self.update()
self._name = '{}_{}'.format(
self._name = name or '{}_{}'.format(
receiver.info['model_name'], receiver.info['identifier'])
self._current_source = None
self._source_list = list(sources.values())
self._source_mapping = sources
self._reverse_mapping = {value: key for key, value in sources.items()}
self.update()
def update(self):
"""Get the latest details from the device."""
@@ -52,8 +82,13 @@ class OnkyoDevice(MediaPlayerDevice):
volume_raw = self._receiver.command('volume query')
mute_raw = self._receiver.command('audio-muting query')
current_source_raw = self._receiver.command('input-selector query')
self._current_source = '_'.join('_'.join(
[i for i in current_source_raw[1]]))
for source in current_source_raw[1]:
if source in self._source_mapping:
self._current_source = self._source_mapping[source]
break
else:
self._current_source = '_'.join(
[i for i in current_source_raw[1]])
self._muted = bool(mute_raw[1] == 'on')
self._volume = int(volume_raw[1], 16)/80.0
@@ -87,6 +122,11 @@ class OnkyoDevice(MediaPlayerDevice):
""""Return the current input source of the device."""
return self._current_source
@property
def source_list(self):
"""List of available input sources."""
return self._source_list
def turn_off(self):
"""Turn off media player."""
self._receiver.command('system-power standby')
@@ -108,4 +148,6 @@ class OnkyoDevice(MediaPlayerDevice):
def select_source(self, source):
"""Set the input source."""
if source in self._source_list:
source = self._reverse_mapping[source]
self._receiver.command('input-selector {}'.format(source))

View File

@@ -0,0 +1,187 @@
"""
Support for the roku media player.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.roku/
"""
import logging
from homeassistant.components.media_player import (
MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME)
REQUIREMENTS = [
'https://github.com/bah2830/python-roku/archive/3.1.1.zip'
'#python-roku==3.1.1']
KNOWN_HOSTS = []
DEFAULT_PORT = 8060
_LOGGER = logging.getLogger(__name__)
SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_SELECT_SOURCE
# pylint: disable=abstract-method
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Roku platform."""
hosts = []
if discovery_info and discovery_info in KNOWN_HOSTS:
return
if discovery_info is not None:
_LOGGER.debug('Discovered Roku: %s', discovery_info[0])
hosts.append(discovery_info[0])
elif CONF_HOST in config:
hosts.append(config[CONF_HOST])
rokus = []
for host in hosts:
rokus.append(RokuDevice(host))
KNOWN_HOSTS.append(host)
add_devices(rokus)
class RokuDevice(MediaPlayerDevice):
"""Representation of a Roku device on the network."""
# pylint: disable=abstract-method
# pylint: disable=too-many-public-methods
def __init__(self, host):
"""Initialize the Roku device."""
from roku import Roku
self.roku = Roku(host)
self.update()
def update(self):
"""Retrieve latest state."""
self.roku_name = "roku_" + self.roku.device_info.sernum
self.ip_address = self.roku.host
self.channels = self.get_source_list()
if self.roku.current_app is not None:
self.current_app = self.roku.current_app
else:
self.current_app = None
def get_source_list(self):
"""Get the list of applications to be used as sources."""
return ["Home"] + sorted(channel.name for channel in self.roku.apps)
@property
def should_poll(self):
"""Device should be polled."""
return True
@property
def name(self):
"""Return the name of the device."""
return self.roku_name
@property
def state(self):
"""Return the state of the device."""
if self.current_app.name in ["Power Saver", "Default screensaver"]:
return STATE_IDLE
elif self.current_app.name == "Roku":
return STATE_HOME
elif self.current_app.name is not None:
return STATE_PLAYING
return STATE_UNKNOWN
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_ROKU
@property
def media_content_type(self):
"""Content type of current playing media."""
if self.current_app is None:
return None
elif self.current_app.name == "Power Saver":
return None
elif self.current_app.name == "Roku":
return None
else:
return MEDIA_TYPE_VIDEO
@property
def media_image_url(self):
"""Image url of current playing media."""
if self.current_app is None:
return None
elif self.current_app.name == "Roku":
return None
elif self.current_app.name == "Power Saver":
return None
elif self.current_app.id is None:
return None
return 'http://{0}:{1}/query/icon/{2}'.format(self.ip_address,
DEFAULT_PORT,
self.current_app.id)
@property
def app_name(self):
"""Name of the current running app."""
return self.current_app.name
@property
def app_id(self):
"""Return the ID of the current running app."""
return self.current_app.id
@property
def source(self):
"""Return the current input source."""
return self.current_app.name
@property
def source_list(self):
"""List of available input sources."""
return self.channels
def media_play_pause(self):
"""Send play/pause command."""
self.roku.play()
def media_previous_track(self):
"""Send previous track command."""
self.roku.reverse()
def media_next_track(self):
"""Send next track command."""
self.roku.forward()
def mute_volume(self, mute):
"""Mute the volume."""
self.roku.volume_mute()
def volume_up(self):
"""Volume up media player."""
self.roku.volume_up()
def volume_down(self):
"""Volume down media player."""
self.roku.volume_down()
def select_source(self, source):
"""Select input source."""
if source == "Home":
self.roku.home()
else:
channel = self.roku[source]
channel.launch()

View File

@@ -145,3 +145,11 @@ select_source:
source:
description: Name of the source to switch to. Platform dependent.
example: 'video1'
sonos_group_players:
description: Send Sonos media player the command for grouping all players into one (party mode).
fields:
entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos'

View File

@@ -7,14 +7,15 @@ https://home-assistant.io/components/media_player.sonos/
import datetime
import logging
import socket
from os import path
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['SoCo==0.11.1']
@@ -32,6 +33,8 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
SUPPORT_SEEK
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -63,9 +66,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.warning('No Sonos speakers found.')
return False
add_devices(SonosDevice(hass, p) for p in players)
devices = [SonosDevice(hass, p) for p in players]
add_devices(devices)
_LOGGER.info('Added %s Sonos speakers', len(players))
def group_players_service(service):
"""Group media players, use player as coordinator."""
entity_id = service.data.get('entity_id')
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
device.group_players()
device.update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS))
return True
@@ -74,16 +99,26 @@ def only_if_coordinator(func):
If used as decorator, avoid calling the decorated method if player is not
a coordinator. If not, a grouped speaker (not in coordinator role) will
throw soco.exceptions.SoCoSlaveException
throw soco.exceptions.SoCoSlaveException.
Also, partially catch exceptions like:
soco.exceptions.SoCoUPnPException: UPnP Error 701 received:
Transition not available from <player ip address>
"""
def wrapper(*args, **kwargs):
"""Decorator wrapper."""
if args[0].is_coordinator:
return func(*args, **kwargs)
from soco.exceptions import SoCoUPnPException
try:
func(*args, **kwargs)
except SoCoUPnPException:
_LOGGER.error('command "%s" for Sonos device "%s" '
'not available in this mode',
func.__name__, args[0].name)
else:
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" '
'(not coordinator)',
func.__name__, args[0].name)
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" (%s)',
func.__name__, args[0].name, 'not coordinator')
return wrapper
@@ -104,7 +139,7 @@ class SonosDevice(MediaPlayerDevice):
@property
def should_poll(self):
"""No polling needed."""
"""Polling needed."""
return True
def update_sonos(self, now):
@@ -258,9 +293,27 @@ class SonosDevice(MediaPlayerDevice):
self._player.play()
@only_if_coordinator
def play_media(self, media_type, media_id):
"""Send the play_media command to the media player."""
self._player.play_uri(media_id)
def play_media(self, media_type, media_id, **kwargs):
"""
Send the play_media command to the media player.
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
"""
if kwargs.get(ATTR_MEDIA_ENQUEUE):
from soco.exceptions import SoCoUPnPException
try:
self._player.add_uri_to_queue(media_id)
except SoCoUPnPException:
_LOGGER.error('Error parsing media uri "%s", '
"please check it's a valid media resource "
'supported by Sonos', media_id)
else:
self._player.play_uri(media_id)
@only_if_coordinator
def group_players(self):
"""Group all players under this coordinator."""
self._player.partymode()
@property
def available(self):

View File

@@ -402,7 +402,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
data = {ATTR_MEDIA_SEEK_POSITION: position}
self._call_service(SERVICE_MEDIA_SEEK, data)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
ATTR_MEDIA_CONTENT_ID: media_id}

View File

@@ -388,6 +388,8 @@ 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,

View File

@@ -1,5 +1,5 @@
"""
Support for Nest thermostats.
Support for Nest thermostats and protect smoke alarms.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.nest/
@@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
REQUIREMENTS = ['python-nest==2.6.0']
REQUIREMENTS = ['python-nest==2.9.2']
DOMAIN = 'nest'
NEST = None
@@ -36,6 +36,16 @@ def devices():
_LOGGER.error("Connection error logging into the nest web service.")
def protect_devices():
"""Generator returning list of protect devices."""
try:
for structure in NEST.structures:
for device in structure.protectdevices:
yield(structure, device)
except socket.error:
_LOGGER.error("Connection error logging into the nest web service.")
# pylint: disable=unused-argument
def setup(hass, config):
"""Setup the Nest thermostat component."""

View File

@@ -0,0 +1,91 @@
"""
AWS Lambda platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.aws_lambda/
"""
import logging
import json
import base64
import voluptuous as vol
from homeassistant.const import (
CONF_PLATFORM, CONF_NAME)
from homeassistant.components.notify import (
ATTR_TARGET, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["boto3==1.3.1"]
CONF_REGION = "region_name"
CONF_ACCESS_KEY_ID = "aws_access_key_id"
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
CONF_PROFILE_NAME = "profile_name"
CONF_CONTEXT = "context"
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "aws_lambda",
vol.Optional(CONF_NAME): vol.Coerce(str),
vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str),
vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str),
vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str),
vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str),
vol.Optional(CONF_CONTEXT, default=dict()): vol.Coerce(dict)
})
def get_service(hass, config):
"""Get the AWS Lambda notification service."""
context_str = json.dumps({'hass': hass.config.as_dict(),
'custom': config[CONF_CONTEXT]})
context_b64 = base64.b64encode(context_str.encode("utf-8"))
context = context_b64.decode("utf-8")
# pylint: disable=import-error
import boto3
aws_config = config.copy()
del aws_config[CONF_PLATFORM]
del aws_config[CONF_NAME]
del aws_config[CONF_CONTEXT]
profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None:
boto3.setup_default_session(profile_name=profile)
del aws_config[CONF_PROFILE_NAME]
lambda_client = boto3.client("lambda", **aws_config)
return AWSLambda(lambda_client, context)
# pylint: disable=too-few-public-methods
class AWSLambda(BaseNotificationService):
"""Implement the notification service for the AWS Lambda service."""
def __init__(self, lambda_client, context):
"""Initialize the service."""
self.client = lambda_client
self.context = context
def send_message(self, message="", **kwargs):
"""Send notification to specified LAMBDA ARN."""
targets = kwargs.get(ATTR_TARGET)
if not targets:
_LOGGER.info("At least 1 target is required")
return
if not isinstance(targets, list):
targets = [targets]
for target in targets:
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
payload = {"message": message}
payload.update(cleaned_kwargs)
self.client.invoke(FunctionName=target,
Payload=json.dumps(payload),
ClientContext=self.context)

View File

@@ -0,0 +1,80 @@
"""
AWS SNS platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.aws_sns/
"""
import logging
import json
import voluptuous as vol
from homeassistant.const import (
CONF_PLATFORM, CONF_NAME)
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TARGET, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["boto3==1.3.1"]
CONF_REGION = "region_name"
CONF_ACCESS_KEY_ID = "aws_access_key_id"
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
CONF_PROFILE_NAME = "profile_name"
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "aws_sns",
vol.Optional(CONF_NAME): vol.Coerce(str),
vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str),
vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str),
vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str),
vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str)
})
def get_service(hass, config):
"""Get the AWS SNS notification service."""
# pylint: disable=import-error
import boto3
aws_config = config.copy()
del aws_config[CONF_PLATFORM]
del aws_config[CONF_NAME]
profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None:
boto3.setup_default_session(profile_name=profile)
del aws_config[CONF_PROFILE_NAME]
sns_client = boto3.client("sns", **aws_config)
return AWSSNS(sns_client)
# pylint: disable=too-few-public-methods
class AWSSNS(BaseNotificationService):
"""Implement the notification service for the AWS SNS service."""
def __init__(self, sns_client):
"""Initialize the service."""
self.client = sns_client
def send_message(self, message="", **kwargs):
"""Send notification to specified SNS ARN."""
targets = kwargs.get(ATTR_TARGET)
if not targets:
_LOGGER.info("At least 1 target is required")
return
if not isinstance(targets, list):
targets = [targets]
message_attributes = {k: {"StringValue": json.dumps(v),
"DataType": "String"}
for k, v in kwargs.items() if v}
for target in targets:
self.client.publish(TargetArn=target, Message=message,
Subject=kwargs.get(ATTR_TITLE),
MessageAttributes=message_attributes)

View File

@@ -0,0 +1,84 @@
"""
AWS SQS platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.aws_sqs/
"""
import logging
import json
import voluptuous as vol
from homeassistant.const import (
CONF_PLATFORM, CONF_NAME)
from homeassistant.components.notify import (
ATTR_TARGET, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["boto3==1.3.1"]
CONF_REGION = "region_name"
CONF_ACCESS_KEY_ID = "aws_access_key_id"
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
CONF_PROFILE_NAME = "profile_name"
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "aws_sqs",
vol.Optional(CONF_NAME): vol.Coerce(str),
vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str),
vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str),
vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str),
vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str)
})
def get_service(hass, config):
"""Get the AWS SQS notification service."""
# pylint: disable=import-error
import boto3
aws_config = config.copy()
del aws_config[CONF_PLATFORM]
del aws_config[CONF_NAME]
profile = aws_config.get(CONF_PROFILE_NAME)
if profile is not None:
boto3.setup_default_session(profile_name=profile)
del aws_config[CONF_PROFILE_NAME]
sqs_client = boto3.client("sqs", **aws_config)
return AWSSQS(sqs_client)
# pylint: disable=too-few-public-methods
class AWSSQS(BaseNotificationService):
"""Implement the notification service for the AWS SQS service."""
def __init__(self, sqs_client):
"""Initialize the service."""
self.client = sqs_client
def send_message(self, message="", **kwargs):
"""Send notification to specified SQS ARN."""
targets = kwargs.get(ATTR_TARGET)
if not targets:
_LOGGER.info("At least 1 target is required")
return
if not isinstance(targets, list):
targets = [targets]
for target in targets:
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
message_body = {"message": message}
message_body.update(cleaned_kwargs)
message_attributes = {}
for key, val in cleaned_kwargs.items():
message_attributes[key] = {"StringValue": json.dumps(val),
"DataType": "String"}
self.client.send_message(QueueUrl=target,
MessageBody=json.dumps(message_body),
MessageAttributes=message_attributes)

View File

@@ -0,0 +1,31 @@
"""
Support for ecobee Send Message service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.ecobee/
"""
import logging
from homeassistant.components import ecobee
from homeassistant.components.notify import BaseNotificationService
DEPENDENCIES = ['ecobee']
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config):
"""Get the Ecobee notification service."""
index = int(config['index']) if 'index' in config else 0
return EcobeeNotificationService(index)
# pylint: disable=too-few-public-methods
class EcobeeNotificationService(BaseNotificationService):
"""Implement the notification service for the Ecobee thermostat."""
def __init__(self, thermostat_index):
"""Initialize the service."""
self.thermostat_index = thermostat_index
def send_message(self, message="", **kwargs):
"""Send a message to a command line."""
ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message)

View File

@@ -41,8 +41,9 @@ class GNTPNotificationService(BaseNotificationService):
# pylint: disable=too-many-arguments
def __init__(self, app_name, app_icon, hostname, password, port):
"""Initialize the service."""
from gntp import notifier
self.gntp = notifier.GrowlNotifier(
import gntp.notifier
import gntp.errors
self.gntp = gntp.notifier.GrowlNotifier(
applicationName=app_name,
notifications=["Notification"],
applicationIcon=app_icon,
@@ -50,7 +51,11 @@ class GNTPNotificationService(BaseNotificationService):
password=password,
port=port
)
self.gntp.register()
try:
self.gntp.register()
except gntp.errors.NetworkError:
_LOGGER.error('Unable to register with the GNTP host.')
return
def send_message(self, message="", **kwargs):
"""Send a message to a user."""

View File

@@ -2,7 +2,7 @@
Google Voice SMS platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.free_mobile/
https://home-assistant.io/components/notify.google_voice/
"""
import logging

View File

@@ -51,7 +51,7 @@ class SlackNotificationService(BaseNotificationService):
"""Send a message to a user."""
import slacker
channel = kwargs.get('target', self._default_channel)
channel = kwargs.get('target') or self._default_channel
try:
self.slack.chat.post_message(channel, message)
except slacker.Error:

View File

@@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-telegram-bot==4.0.1']
REQUIREMENTS = ['python-telegram-bot==4.1.1']
def get_service(hass, config):

View File

@@ -0,0 +1,62 @@
"""
Twilio SMS platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.twilio_sms/
"""
import logging
from homeassistant.components.notify import (
ATTR_TARGET, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["twilio==5.4.0"]
CONF_ACCOUNT_SID = "account_sid"
CONF_AUTH_TOKEN = "auth_token"
CONF_FROM_NUMBER = "from_number"
def get_service(hass, config):
"""Get the Twilio SMS notification service."""
if not validate_config({DOMAIN: config},
{DOMAIN: [CONF_ACCOUNT_SID,
CONF_AUTH_TOKEN,
CONF_FROM_NUMBER]},
_LOGGER):
return None
# pylint: disable=import-error
from twilio.rest import TwilioRestClient
twilio_client = TwilioRestClient(config[CONF_ACCOUNT_SID],
config[CONF_AUTH_TOKEN])
return TwilioSMSNotificationService(twilio_client,
config[CONF_FROM_NUMBER])
# pylint: disable=too-few-public-methods
class TwilioSMSNotificationService(BaseNotificationService):
"""Implement the notification service for the Twilio SMS service."""
def __init__(self, twilio_client, from_number):
"""Initialize the service."""
self.client = twilio_client
self.from_number = from_number
def send_message(self, message="", **kwargs):
"""Send SMS to specified target user cell."""
targets = kwargs.get(ATTR_TARGET)
if not targets:
_LOGGER.info("At least 1 target is required")
return
if not isinstance(targets, list):
targets = [targets]
for target in targets:
self.client.messages.create(to=target, body=message,
from_=self.from_number)

View File

@@ -0,0 +1,150 @@
"""
Support for Qwikswitch devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/qwikswitch/
"""
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.discovery import load_platform
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.3.zip'
'#pyqwikswitch==0.3']
DEPENDENCIES = []
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'qwikswitch'
QSUSB = None
class QSToggleEntity(object):
"""Representation of a Qwikswitch Entity.
Implement base QS methods. Modeled around HA ToggleEntity[1] & should only
be used in a class that extends both QSToggleEntity *and* ToggleEntity.
Implemented:
- QSLight extends QSToggleEntity and Light[2] (ToggleEntity[1])
- QSSwitch extends QSToggleEntity and SwitchDevice[3] (ToggleEntity[1])
[1] /helpers/entity.py
[2] /components/light/__init__.py
[3] /components/switch/__init__.py
"""
def __init__(self, qsitem, qsusb):
"""Initialize the ToggleEntity."""
from pyqwikswitch import (QS_ID, QS_NAME, QSType, PQS_VALUE, PQS_TYPE)
self._id = qsitem[QS_ID]
self._name = qsitem[QS_NAME]
self._value = qsitem[PQS_VALUE]
self._qsusb = qsusb
self._dim = qsitem[PQS_TYPE] == QSType.dimmer
@property
def brightness(self):
"""Return the brightness of this light between 0..100."""
return self._value if self._dim else None
# pylint: disable=no-self-use
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the light."""
return self._name
@property
def is_on(self):
"""Check if device is on (non-zero)."""
return self._value > 0
def update_value(self, value):
"""Decode the QSUSB value and update the Home assistant state."""
if value != self._value:
self._value = value
# pylint: disable=no-member
super().update_ha_state() # Part of Entity/ToggleEntity
return self._value
def turn_on(self, **kwargs):
"""Turn the device on."""
newvalue = 255
if ATTR_BRIGHTNESS in kwargs:
newvalue = kwargs[ATTR_BRIGHTNESS]
if self._qsusb.set(self._id, round(min(newvalue, 255)/2.55)) >= 0:
self.update_value(newvalue)
# pylint: disable=unused-argument
def turn_off(self, **kwargs):
"""Turn the device off."""
if self._qsusb.set(self._id, 0) >= 0:
self.update_value(0)
# pylint: disable=too-many-locals
def setup(hass, config):
"""Setup the QSUSB component."""
from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD,
QS_TYPE, PQS_VALUE, PQS_TYPE, QSType)
# Override which cmd's in /&listen packets will fire events
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS))
cmd_buttons = cmd_buttons.split(',')
try:
url = config[DOMAIN].get('url', 'http://127.0.0.1:2020')
dimmer_adjust = float(config[DOMAIN].get('dimmer_adjust', '1'))
qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
# Ensure qsusb terminates threads correctly
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: qsusb.stop())
except ValueError as val_err:
_LOGGER.error(str(val_err))
return False
qsusb.ha_devices = qsusb.devices()
qsusb.ha_objects = {}
# Identify switches & remove ' Switch' postfix in name
for item in qsusb.ha_devices:
if item[PQS_TYPE] == QSType.relay and \
item[QS_NAME].lower().endswith(' switch'):
item[QS_TYPE] = 'switch'
item[QS_NAME] = item[QS_NAME][:-7]
global QSUSB
if QSUSB is None:
QSUSB = {}
QSUSB[id(qsusb)] = qsusb
# Load sub-components for qwikswitch
for comp_name in ('switch', 'light'):
load_platform(hass, comp_name, 'qwikswitch',
{'qsusb_id': id(qsusb)}, config)
def qs_callback(item):
"""Typically a button press or update signal."""
# If button pressed, fire a hass event
if item.get(QS_CMD, '') in cmd_buttons:
hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id'))
return
# Update all ha_objects
qsreply = qsusb.devices()
if qsreply is False:
return
for item in qsreply:
if item[QS_ID] in qsusb.ha_objects:
qsusb.ha_objects[item[QS_ID]].update_value(
round(min(item[PQS_VALUE], 100) * 2.55))
qsusb.listen(callback=qs_callback, timeout=30)
return True

View File

@@ -13,7 +13,8 @@ import logging
import queue
import sqlite3
import threading
from datetime import date, datetime
from datetime import date, datetime, timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.const import (
@@ -21,6 +22,7 @@ from homeassistant.const import (
EVENT_TIME_CHANGED, MATCH_ALL)
from homeassistant.core import Event, EventOrigin, State
from homeassistant.remote import JSONEncoder
from homeassistant.helpers.event import track_point_in_utc_time
DOMAIN = "recorder"
@@ -30,6 +32,15 @@ RETURN_ROWCOUNT = "rowcount"
RETURN_LASTROWID = "lastrowid"
RETURN_ONE_ROW = "one_row"
CONF_PURGE_DAYS = "purge_days"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
vol.Range(min=1)),
})
}, extra=vol.ALLOW_EXTRA)
_INSTANCE = None
_LOGGER = logging.getLogger(__name__)
@@ -102,14 +113,14 @@ def setup(hass, config):
"""Setup the recorder."""
# pylint: disable=global-statement
global _INSTANCE
_INSTANCE = Recorder(hass)
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
_INSTANCE = Recorder(hass, purge_days=purge_days)
return True
class RecorderRun(object):
"""Representation of arecorder run."""
"""Representation of a recorder run."""
def __init__(self, row=None):
"""Initialize the recorder run."""
@@ -169,11 +180,12 @@ class Recorder(threading.Thread):
"""A threaded recorder class."""
# pylint: disable=too-many-instance-attributes
def __init__(self, hass):
def __init__(self, hass, purge_days):
"""Initialize the recorder."""
threading.Thread.__init__(self)
self.hass = hass
self.purge_days = purge_days
self.conn = None
self.queue = queue.Queue()
self.quit_object = object()
@@ -194,6 +206,10 @@ class Recorder(threading.Thread):
"""Start processing events to save."""
self._setup_connection()
self._setup_run()
if self.purge_days is not None:
track_point_in_utc_time(self.hass,
lambda now: self._purge_old_data(),
dt_util.utcnow() + timedelta(minutes=5))
while True:
event = self.queue.get()
@@ -475,6 +491,32 @@ class Recorder(threading.Thread):
"UPDATE recorder_runs SET end=? WHERE start=?",
(dt_util.utcnow(), self.recording_start))
def _purge_old_data(self):
"""Purge events and states older than purge_days ago."""
if not self.purge_days or self.purge_days < 1:
_LOGGER.debug("purge_days set to %s, will not purge any old data.",
self.purge_days)
return
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
_LOGGER.info("Purging events created before %s", purge_before)
deleted_rows = self.query(
sql_query="DELETE FROM events WHERE created < ?;",
data=(int(purge_before.timestamp()),),
return_value=RETURN_ROWCOUNT)
_LOGGER.debug("Deleted %s events", deleted_rows)
_LOGGER.info("Purging states created before %s", purge_before)
deleted_rows = self.query(
sql_query="DELETE FROM states WHERE created < ?;",
data=(int(purge_before.timestamp()),),
return_value=RETURN_ROWCOUNT)
_LOGGER.debug("Deleted %s states", deleted_rows)
# Execute sqlite vacuum command to free up space on disk
self.query("VACUUM;")
def _adapt_datetime(datetimestamp):
"""Turn a datetime into an integer for in the DB."""

View File

@@ -2,7 +2,7 @@
Support for Powerview scenes from a Powerview hub.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/scene/
https://home-assistant.io/components/scene.hunterdouglas_powerview/
"""
import logging

View File

@@ -10,7 +10,7 @@ import logging
import datetime
import time
from homeassistant.const import HTTP_OK
from homeassistant.const import HTTP_OK, TEMP_CELSIUS
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
@@ -85,15 +85,15 @@ FITBIT_MEASUREMENTS = {
"liquids": "fl. oz.",
"blood glucose": "mg/dL",
},
"en_UK": {
"en_GB": {
"duration": "milliseconds",
"distance": "kilometers",
"elevation": "meters",
"height": "centimeters",
"weight": "stone",
"body": "centimeters",
"liquids": "millileters",
"blood glucose": "mmol/l"
"liquids": "milliliters",
"blood glucose": "mmol/L"
},
"metric": {
"duration": "milliseconds",
@@ -102,8 +102,8 @@ FITBIT_MEASUREMENTS = {
"height": "centimeters",
"weight": "kilograms",
"body": "centimeters",
"liquids": "millileters",
"blood glucose": "mmol/l"
"liquids": "milliliters",
"blood glucose": "mmol/L"
}
}
@@ -153,7 +153,8 @@ def request_app_setup(hass, config, add_devices, config_path,
else:
setup_platform(hass, config, add_devices, discovery_info)
start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_START)
start_url = "{}{}".format(hass.config.api.base_url,
FITBIT_AUTH_CALLBACK_PATH)
description = """Please create a Fitbit developer app at
https://dev.fitbit.com/apps/new.
@@ -222,8 +223,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
access_token = config_file.get("access_token")
refresh_token = config_file.get("refresh_token")
if None not in (access_token, refresh_token):
authd_client = fitbit.Fitbit(config.get("client_id"),
config.get("client_secret"),
authd_client = fitbit.Fitbit(config_file.get("client_id"),
config_file.get("client_secret"),
access_token=access_token,
refresh_token=refresh_token)
@@ -235,12 +236,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = []
for resource in config.get("monitored_resources",
FITBIT_DEFAULT_RESOURCE_LIST):
dev.append(FitbitSensor(authd_client, config_path, resource))
dev.append(FitbitSensor(authd_client, config_path, resource,
hass.config.temperature_unit ==
TEMP_CELSIUS))
add_devices(dev)
else:
oauth = fitbit.api.FitbitOauth2Client(config.get("client_id"),
config.get("client_secret"))
oauth = fitbit.api.FitbitOauth2Client(config_file.get("client_id"),
config_file.get("client_secret"))
redirect_uri = "{}{}".format(hass.config.api.base_url,
FITBIT_AUTH_CALLBACK_PATH)
@@ -301,9 +304,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
setup_platform(hass, config, add_devices, discovery_info=None)
hass.http.register_path("GET", FITBIT_AUTH_START, _start_fitbit_auth)
hass.http.register_path("GET", FITBIT_AUTH_START, _start_fitbit_auth,
require_auth=False)
hass.http.register_path("GET", FITBIT_AUTH_CALLBACK_PATH,
_finish_fitbit_auth)
_finish_fitbit_auth, require_auth=False)
request_oauth_completion(hass)
@@ -312,8 +316,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class FitbitSensor(Entity):
"""Implementation of a Fitbit sensor."""
def __init__(self, client, config_path, resource_type):
"""Initialize the Uber sensor."""
def __init__(self, client, config_path, resource_type, is_metric):
"""Initialize the Fitbit sensor."""
self.client = client
self.config_path = config_path
self.resource_type = resource_type
@@ -326,7 +330,13 @@ class FitbitSensor(Entity):
unit_type = FITBIT_RESOURCES_LIST[self.resource_type]
if unit_type == "":
split_resource = self.resource_type.split("/")
measurement_system = FITBIT_MEASUREMENTS[self.client.system]
try:
measurement_system = FITBIT_MEASUREMENTS[self.client.system]
except KeyError:
if is_metric:
measurement_system = FITBIT_MEASUREMENTS["metric"]
else:
measurement_system = FITBIT_MEASUREMENTS["en_US"]
unit_type = measurement_system[split_resource[-1]]
self._unit_of_measurement = unit_type
self._state = 0

View File

@@ -10,8 +10,10 @@ import logging
import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -23,47 +25,102 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
CONF_ORIGIN = 'origin'
CONF_DESTINATION = 'destination'
CONF_TRAVEL_MODE = 'travel_mode'
CONF_OPTIONS = 'options'
CONF_MODE = 'mode'
CONF_NAME = 'name'
ALL_LANGUAGES = ['ar', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es',
'eu', 'fa', 'fi', 'fr', 'gl', 'gu', 'hi', 'hr', 'hu', 'id',
'it', 'iw', 'ja', 'kn', 'ko', 'lt', 'lv', 'ml', 'mr', 'nl',
'no', 'pl', 'pt', 'pt-BR', 'pt-PT', 'ro', 'ru', 'sk', 'sl',
'sr', 'sv', 'ta', 'te', 'th', 'tl', 'tr', 'uk', 'vi',
'zh-CN', 'zh-TW']
TRANSIT_PREFS = ['less_walking', 'fewer_transfers']
PLATFORM_SCHEMA = vol.Schema({
vol.Required('platform'): 'google_travel_time',
vol.Optional(CONF_NAME): vol.Coerce(str),
vol.Required(CONF_API_KEY): vol.Coerce(str),
vol.Required(CONF_ORIGIN): vol.Coerce(str),
vol.Required(CONF_DESTINATION): vol.Coerce(str),
vol.Optional(CONF_TRAVEL_MODE, default='driving'):
vol.In(["driving", "walking", "bicycling", "transit"])
vol.Optional(CONF_TRAVEL_MODE):
vol.In(["driving", "walking", "bicycling", "transit"]),
vol.Optional(CONF_OPTIONS, default=dict()): vol.All(
dict, vol.Schema({
vol.Optional(CONF_MODE, default='driving'):
vol.In(["driving", "walking", "bicycling", "transit"]),
vol.Optional('language'): vol.In(ALL_LANGUAGES),
vol.Optional('avoid'): vol.In(['tolls', 'highways',
'ferries', 'indoor']),
vol.Optional('units'): vol.In(['metric', 'imperial']),
vol.Exclusive('arrival_time', 'time'): cv.string,
vol.Exclusive('departure_time', 'time'): cv.string,
vol.Optional('traffic_model'): vol.In(['best_guess',
'pessimistic',
'optimistic']),
vol.Optional('transit_mode'): vol.In(['bus', 'subway', 'train',
'tram', 'rail']),
vol.Optional('transit_routing_preference'): vol.In(TRANSIT_PREFS)
}))
})
def convert_time_to_utc(timestr):
"""Take a string like 08:00:00 and convert it to a unix timestamp."""
combined = datetime.combine(dt_util.start_of_local_day(),
dt_util.parse_time(timestr))
if combined < datetime.now():
combined = combined + timedelta(days=1)
return dt_util.as_timestamp(combined)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the travel time platform."""
# pylint: disable=too-many-locals
options = config.get(CONF_OPTIONS)
is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
if options.get('units') is None:
if hass.config.temperature_unit is TEMP_CELSIUS:
options['units'] = 'metric'
elif hass.config.temperature_unit is TEMP_FAHRENHEIT:
options['units'] = 'imperial'
travel_mode = config.get(CONF_TRAVEL_MODE)
mode = options.get(CONF_MODE)
if travel_mode is not None:
wstr = ("Google Travel Time: travel_mode is deprecated, please add "
"mode to the options dictionary instead!")
_LOGGER.warning(wstr)
if mode is None:
options[CONF_MODE] = travel_mode
titled_mode = options.get(CONF_MODE).title()
formatted_name = "Google Travel Time - {}".format(titled_mode)
name = config.get(CONF_NAME, formatted_name)
api_key = config.get(CONF_API_KEY)
origin = config.get(CONF_ORIGIN)
destination = config.get(CONF_DESTINATION)
travel_mode = config.get(CONF_TRAVEL_MODE)
sensor = GoogleTravelTimeSensor(api_key, origin, destination,
travel_mode, is_metric)
sensor = GoogleTravelTimeSensor(name, api_key, origin, destination,
options)
if sensor.valid_api_connection:
add_devices_callback([sensor])
# pylint: disable=too-many-instance-attributes
class GoogleTravelTimeSensor(Entity):
"""Representation of a tavel time sensor."""
# pylint: disable=too-many-arguments
def __init__(self, api_key, origin, destination, travel_mode, is_metric):
def __init__(self, name, api_key, origin, destination, options):
"""Initialize the sensor."""
if is_metric:
self._unit = 'metric'
else:
self._unit = 'imperial'
self._name = name
self._options = options
self._origin = origin
self._destination = destination
self._travel_mode = travel_mode
self._matrix = None
self.valid_api_connection = True
@@ -79,17 +136,23 @@ class GoogleTravelTimeSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
return self._matrix['rows'][0]['elements'][0]['duration']['value']/60.0
_data = self._matrix['rows'][0]['elements'][0]
if 'duration_in_traffic' in _data:
return round(_data['duration_in_traffic']['value']/60)
if 'duration' in _data:
return round(_data['duration']['value']/60)
return None
@property
def name(self):
"""Get the name of the sensor."""
return "Google Travel time"
return self._name
@property
def device_state_attributes(self):
"""Return the state attributes."""
res = self._matrix.copy()
res.update(self._options)
del res['rows']
_data = self._matrix['rows'][0]['elements'][0]
if 'duration_in_traffic' in _data:
@@ -108,10 +171,21 @@ class GoogleTravelTimeSensor(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from Google."""
now = datetime.now()
options_copy = self._options.copy()
dtime = options_copy.get('departure_time')
atime = options_copy.get('arrival_time')
if dtime is not None and ':' in dtime:
options_copy['departure_time'] = convert_time_to_utc(dtime)
elif dtime is not None:
options_copy['departure_time'] = dtime
else:
options_copy['departure_time'] = 'now'
if atime is not None and ':' in atime:
options_copy['arrival_time'] = convert_time_to_utc(atime)
elif atime is not None:
options_copy['arrival_time'] = atime
self._matrix = self._client.distance_matrix(self._origin,
self._destination,
mode=self._travel_mode,
units=self._unit,
departure_time=now,
traffic_model="optimistic")
**options_copy)

View File

@@ -7,14 +7,15 @@ https://home-assistant.io/components/sensor.gtfs/
import os
import logging
import datetime
import threading
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["https://github.com/robbiet480/pygtfs/archive/"
"432414b720c580fb2667a0a48f539118a2d95969.zip#"
"pygtfs==0.1.2"]
"00546724e4bbcb3053110d844ca44e2246267dd8.zip#"
"pygtfs==0.1.3"]
ICON = "mdi:train"
@@ -152,9 +153,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error("The given GTFS data file/folder was not found!")
return False
import pygtfs
split_file_name = os.path.splitext(config["data"])
sqlite_file = "{}.sqlite".format(split_file_name[0])
joined_path = os.path.join(gtfs_dir, sqlite_file)
gtfs = pygtfs.Schedule(joined_path)
# pylint: disable=no-member
if len(gtfs.feeds) < 1:
pygtfs.append_feed(gtfs, os.path.join(gtfs_dir,
config["data"]))
dev = []
dev.append(GTFSDepartureSensor(config["data"], gtfs_dir,
config["origin"], config["destination"]))
dev.append(GTFSDepartureSensor(gtfs, config["origin"],
config["destination"]))
add_devices(dev)
# pylint: disable=too-many-instance-attributes,too-few-public-methods
@@ -163,16 +177,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class GTFSDepartureSensor(Entity):
"""Implementation of an GTFS departures sensor."""
def __init__(self, data_source, gtfs_folder, origin, destination):
def __init__(self, pygtfs, origin, destination):
"""Initialize the sensor."""
self._data_source = data_source
self._gtfs_folder = gtfs_folder
self._pygtfs = pygtfs
self.origin = origin
self.destination = destination
self._name = "GTFS Sensor"
self._unit_of_measurement = "min"
self._state = 0
self._attributes = {}
self.lock = threading.Lock()
self.update()
@property
@@ -202,62 +216,52 @@ class GTFSDepartureSensor(Entity):
def update(self):
"""Get the latest data from GTFS and update the states."""
import pygtfs
with self.lock:
self._departure = get_next_departure(self._pygtfs, self.origin,
self.destination)
self._state = self._departure["minutes_until_departure"]
split_file_name = os.path.splitext(self._data_source)
origin_station = self._departure["origin_station"]
destination_station = self._departure["destination_station"]
origin_stop_time = self._departure["origin_stop_time"]
destination_stop_time = self._departure["destination_stop_time"]
agency = self._departure["agency"]
route = self._departure["route"]
trip = self._departure["trip"]
sqlite_file = "{}.sqlite".format(split_file_name[0])
gtfs = pygtfs.Schedule(os.path.join(self._gtfs_folder, sqlite_file))
name = "{} {} to {} next departure"
self._name = name.format(agency.agency_name,
origin_station.stop_id,
destination_station.stop_id)
# pylint: disable=no-member
if len(gtfs.feeds) < 1:
pygtfs.append_feed(gtfs, os.path.join(self._gtfs_folder,
self._data_source))
# Build attributes
self._departure = get_next_departure(gtfs, self.origin,
self.destination)
self._state = self._departure["minutes_until_departure"]
self._attributes = {}
origin_station = self._departure["origin_station"]
destination_station = self._departure["destination_station"]
origin_stop_time = self._departure["origin_stop_time"]
destination_stop_time = self._departure["destination_stop_time"]
agency = self._departure["agency"]
route = self._departure["route"]
trip = self._departure["trip"]
def dict_for_table(resource):
"""Return a dict for the SQLAlchemy resource given."""
return dict((col, getattr(resource, col))
for col in resource.__table__.columns.keys())
name = "{} {} to {} next departure"
self._name = name.format(agency.agency_name,
origin_station.stop_id,
destination_station.stop_id)
def append_keys(resource, prefix=None):
"""Properly format key val pairs to append to attributes."""
for key, val in resource.items():
if val == "" or val is None or key == "feed_id":
continue
pretty_key = key.replace("_", " ")
pretty_key = pretty_key.title()
pretty_key = pretty_key.replace("Id", "ID")
pretty_key = pretty_key.replace("Url", "URL")
if prefix is not None and \
pretty_key.startswith(prefix) is False:
pretty_key = "{} {}".format(prefix, pretty_key)
self._attributes[pretty_key] = val
# Build attributes
self._attributes = {}
def dict_for_table(resource):
"""Return a dict for the SQLAlchemy resource given."""
return dict((col, getattr(resource, col))
for col in resource.__table__.columns.keys())
def append_keys(resource, prefix=None):
"""Properly format key val pairs to append to attributes."""
for key, val in resource.items():
if val == "" or val is None or key == "feed_id":
continue
pretty_key = key.replace("_", " ")
pretty_key = pretty_key.title()
pretty_key = pretty_key.replace("Id", "ID")
pretty_key = pretty_key.replace("Url", "URL")
if prefix is not None and \
pretty_key.startswith(prefix) is False:
pretty_key = "{} {}".format(prefix, pretty_key)
self._attributes[pretty_key] = val
append_keys(dict_for_table(agency), "Agency")
append_keys(dict_for_table(route), "Route")
append_keys(dict_for_table(trip), "Trip")
append_keys(dict_for_table(origin_station), "Origin Station")
append_keys(dict_for_table(destination_station), "Destination Station")
append_keys(origin_stop_time, "Origin Stop")
append_keys(destination_stop_time, "Destination Stop")
append_keys(dict_for_table(agency), "Agency")
append_keys(dict_for_table(route), "Route")
append_keys(dict_for_table(trip), "Trip")
append_keys(dict_for_table(origin_station), "Origin Station")
append_keys(dict_for_table(destination_station),
"Destination Station")
append_keys(origin_stop_time, "Origin Stop")
append_keys(destination_stop_time, "Destination Stop")

View File

@@ -0,0 +1,90 @@
"""
Sensor for Last.fm account status.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.lastfm/
"""
import re
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_API_KEY
ICON = 'mdi:lastfm'
REQUIREMENTS = ['pylast==1.6.0']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Last.fm platform."""
import pylast as lastfm
network = lastfm.LastFMNetwork(api_key=config.get(CONF_API_KEY))
add_devices(
[LastfmSensor(username,
network) for username in config.get("users", [])])
class LastfmSensor(Entity):
"""A class for the Last.fm account."""
# pylint: disable=abstract-method, too-many-instance-attributes
def __init__(self, user, lastfm):
"""Initialize the sensor."""
self._user = lastfm.get_user(user)
self._name = user
self._lastfm = lastfm
self._state = "Not Scrobbling"
self._playcount = None
self._lastplayed = None
self._topplayed = None
self._cover = None
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def entity_id(self):
"""Return the entity ID."""
return 'sensor.lastfm_{}'.format(self._name)
@property
def state(self):
"""Return the state of the sensor."""
return self._state
# pylint: disable=no-member
def update(self):
"""Update device state."""
self._cover = self._user.get_image()
self._playcount = self._user.get_playcount()
last = self._user.get_recent_tracks(limit=2)[0]
self._lastplayed = "{} - {}".format(last.track.artist,
last.track.title)
top = self._user.get_top_tracks(limit=1)[0]
toptitle = re.search("', '(.+?)',", str(top))
topartist = re.search("'(.+?)',", str(top))
self._topplayed = "{} - {}".format(topartist.group(1),
toptitle.group(1))
if self._user.get_now_playing() is None:
self._state = "Not Scrobbling"
return
now = self._user.get_now_playing()
self._state = "{} - {}".format(now.artist, now.title)
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {'Play Count': self._playcount, 'Last Played':
self._lastplayed, 'Top Played': self._topplayed}
@property
def entity_picture(self):
"""Avatar of the user."""
return self._cover
@property
def icon(self):
"""Return the icon to use in the frontend."""
return ICON

View File

@@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "loopenergy"
REQUIREMENTS = ['pyloopenergy==0.0.10']
REQUIREMENTS = ['pyloopenergy==0.0.12']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@@ -0,0 +1,268 @@
"""
Calculates mold growth indication from temperature and humidity.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mold_indicator/
"""
import logging
import math
import homeassistant.util as util
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_state_change
from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Mold Indicator"
CONF_INDOOR_TEMP = "indoor_temp_sensor"
CONF_OUTDOOR_TEMP = "outdoor_temp_sensor"
CONF_INDOOR_HUMIDITY = "indoor_humidity_sensor"
CONF_CALIBRATION_FACTOR = "calibration_factor"
MAGNUS_K2 = 17.62
MAGNUS_K3 = 243.12
ATTR_DEWPOINT = "Dewpoint"
ATTR_CRITICAL_TEMP = "Est. Crit. Temp"
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup MoldIndicator sensor."""
name = config.get('name', DEFAULT_NAME)
indoor_temp_sensor = config.get(CONF_INDOOR_TEMP)
outdoor_temp_sensor = config.get(CONF_OUTDOOR_TEMP)
indoor_humidity_sensor = config.get(CONF_INDOOR_HUMIDITY)
calib_factor = util.convert(config.get(CONF_CALIBRATION_FACTOR),
float, None)
if None in (indoor_temp_sensor,
outdoor_temp_sensor, indoor_humidity_sensor):
_LOGGER.error('Missing required key %s, %s or %s',
CONF_INDOOR_TEMP, CONF_OUTDOOR_TEMP,
CONF_INDOOR_HUMIDITY)
return False
add_devices_callback([MoldIndicator(
hass, name, indoor_temp_sensor,
outdoor_temp_sensor, indoor_humidity_sensor,
calib_factor)])
# pylint: disable=too-many-instance-attributes
class MoldIndicator(Entity):
"""Represents a MoldIndication sensor."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, indoor_temp_sensor, outdoor_temp_sensor,
indoor_humidity_sensor, calib_factor):
"""Initialize the sensor."""
self._state = None
self._name = name
self._indoor_temp_sensor = indoor_temp_sensor
self._indoor_humidity_sensor = indoor_humidity_sensor
self._outdoor_temp_sensor = outdoor_temp_sensor
self._calib_factor = calib_factor
self._is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
self._dewpoint = None
self._indoor_temp = None
self._outdoor_temp = None
self._indoor_hum = None
self._crit_temp = None
track_state_change(hass, indoor_temp_sensor, self._sensor_changed)
track_state_change(hass, outdoor_temp_sensor, self._sensor_changed)
track_state_change(hass, indoor_humidity_sensor, self._sensor_changed)
# Read initial state
indoor_temp = hass.states.get(indoor_temp_sensor)
outdoor_temp = hass.states.get(outdoor_temp_sensor)
indoor_hum = hass.states.get(indoor_humidity_sensor)
if indoor_temp:
self._indoor_temp = \
MoldIndicator._update_temp_sensor(indoor_temp)
if outdoor_temp:
self._outdoor_temp = \
MoldIndicator._update_temp_sensor(outdoor_temp)
if indoor_hum:
self._indoor_hum = \
MoldIndicator._update_hum_sensor(indoor_hum)
self.update()
@staticmethod
def _update_temp_sensor(state):
"""Parse temperature sensor value."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
temp = util.convert(state.state, float)
if temp is None:
_LOGGER.error('Unable to parse sensor temperature: %s',
state.state)
return None
# convert to celsius if necessary
if unit == TEMP_FAHRENHEIT:
return util.temperature.fahrenheit_to_celcius(temp)
elif unit == TEMP_CELSIUS:
return temp
else:
_LOGGER.error("Temp sensor has unsupported unit: %s"
" (allowed: %s, %s)",
unit, TEMP_CELSIUS, TEMP_FAHRENHEIT)
return None
@staticmethod
def _update_hum_sensor(state):
"""Parse humidity sensor value."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
hum = util.convert(state.state, float)
if hum is None:
_LOGGER.error('Unable to parse sensor humidity: %s',
state.state)
return None
# check unit
if unit != "%":
_LOGGER.error(
"Humidity sensor has unsupported unit: %s %s",
unit,
" (allowed: %)")
# check range
if hum > 100 or hum < 0:
_LOGGER.error(
"Humidity sensor out of range: %s %s",
hum,
" (allowed: 0-100%)")
return hum
def update(self):
"""Calculate latest state."""
# check all sensors
if None in (self._indoor_temp, self._indoor_hum, self._outdoor_temp):
return
# re-calculate dewpoint and mold indicator
self._calc_dewpoint()
self._calc_moldindicator()
def _sensor_changed(self, entity_id, old_state, new_state):
"""Called when sensor values change."""
if new_state is None:
return
if entity_id == self._indoor_temp_sensor:
# update the indoor temp sensor
self._indoor_temp = MoldIndicator._update_temp_sensor(new_state)
elif entity_id == self._outdoor_temp_sensor:
# update outdoor temp sensor
self._outdoor_temp = MoldIndicator._update_temp_sensor(new_state)
elif entity_id == self._indoor_humidity_sensor:
# update humidity
self._indoor_hum = MoldIndicator._update_hum_sensor(new_state)
self.update()
self.update_ha_state()
def _calc_dewpoint(self):
"""Calculate the dewpoint for the indoor air."""
# use magnus approximation to calculate the dew point
alpha = MAGNUS_K2 * self._indoor_temp / (MAGNUS_K3 + self._indoor_temp)
beta = MAGNUS_K2 * MAGNUS_K3 / (MAGNUS_K3 + self._indoor_temp)
if self._indoor_hum == 0:
self._dewpoint = -50 # not defined, assume very low value
else:
self._dewpoint = \
MAGNUS_K3 * (alpha + math.log(self._indoor_hum / 100.0)) / \
(beta - math.log(self._indoor_hum / 100.0))
_LOGGER.debug("Dewpoint: %f " + TEMP_CELSIUS, self._dewpoint)
def _calc_moldindicator(self):
"""Calculate the humidity at the (cold) calibration point."""
if None in (self._dewpoint, self._calib_factor) or \
self._calib_factor == 0:
_LOGGER.debug("Invalid inputs - dewpoint: %s,"
" calibration-factor: %s",
self._dewpoint, self._calib_factor)
self._state = None
return
# first calculate the approximate temperature at the calibration point
self._crit_temp = \
self._outdoor_temp + (self._indoor_temp - self._outdoor_temp) / \
self._calib_factor
_LOGGER.debug(
"Estimated Critical Temperature: %f " +
TEMP_CELSIUS, self._crit_temp)
# Then calculate the humidity at this point
alpha = MAGNUS_K2 * self._crit_temp / (MAGNUS_K3 + self._crit_temp)
beta = MAGNUS_K2 * MAGNUS_K3 / (MAGNUS_K3 + self._crit_temp)
crit_humidity = \
math.exp(
(self._dewpoint * beta - MAGNUS_K3 * alpha) /
(self._dewpoint + MAGNUS_K3)) * 100.0
# check bounds and format
if crit_humidity > 100:
self._state = '100'
elif crit_humidity < 0:
self._state = '0'
else:
self._state = '{0:d}'.format(int(crit_humidity))
_LOGGER.debug('Mold indicator humidity: %s ', self._state)
@property
def should_poll(self):
"""Polling needed."""
return False
@property
def name(self):
"""Return the name."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return "%"
@property
def state(self):
"""Return the state of the entity."""
return self._state
@property
def state_attributes(self):
"""Return the state attributes."""
if self._is_metric:
return {
ATTR_DEWPOINT: self._dewpoint,
ATTR_CRITICAL_TEMP: self._crit_temp,
}
else:
return {
ATTR_DEWPOINT:
util.temperature.celcius_to_fahrenheit(
self._dewpoint),
ATTR_CRITICAL_TEMP:
util.temperature.celcius_to_fahrenheit(
self._crit_temp),
}

View File

@@ -4,6 +4,8 @@ Support for Nest Thermostat Sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.nest/
"""
from itertools import chain
import voluptuous as vol
import homeassistant.components.nest as nest
@@ -29,9 +31,13 @@ WEATHER_VARS = {'weather_humidity': 'humidity',
SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V',
'kph': 'kph', 'temperature': '°C'}
PROTECT_VARS = ['co_status',
'smoke_status',
'battery_level']
SENSOR_TEMP_TYPES = ['temperature', 'target']
_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + \
_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS + \
list(WEATHER_VARS.keys())
PLATFORM_SCHEMA = vol.Schema({
@@ -44,20 +50,34 @@ PLATFORM_SCHEMA = vol.Schema({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest Sensor."""
for structure, device in nest.devices():
for structure, device in chain(nest.devices(), nest.protect_devices()):
sensors = [NestBasicSensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in SENSOR_TYPES]
if variable in SENSOR_TYPES and is_thermostat(device)]
sensors += [NestTempSensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in SENSOR_TEMP_TYPES]
if variable in SENSOR_TEMP_TYPES and is_thermostat(device)]
sensors += [NestWeatherSensor(structure, device,
WEATHER_VARS[variable])
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in WEATHER_VARS]
if variable in WEATHER_VARS and is_thermostat(device)]
sensors += [NestProtectSensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in PROTECT_VARS and is_protect(device)]
add_devices(sensors)
def is_thermostat(device):
"""Target devices that are Nest Thermostats."""
return bool(device.__class__.__name__ == 'Device')
def is_protect(device):
"""Target devices that are Nest Protect Smoke Alarms."""
return bool(device.__class__.__name__ == 'ProtectDevice')
class NestSensor(Entity):
"""Representation of a Nest sensor."""
@@ -130,3 +150,28 @@ class NestWeatherSensor(NestSensor):
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return SENSOR_UNITS.get(self.variable, None)
class NestProtectSensor(NestSensor):
"""Return the state of nest protect."""
@property
def state(self):
"""Return the state of the sensor."""
state = getattr(self.device, self.variable)
if self.variable == 'battery_level':
return getattr(self.device, self.variable)
else:
if state == 0:
return 'Ok'
if state == 1 or state == 2:
return 'Warning'
if state == 3:
return 'Emergency'
return 'Unknown'
@property
def name(self):
"""Return the name of the nest, if any."""
return "{} {}".format(self.device.where.capitalize(), self.variable)

View File

@@ -7,11 +7,12 @@ https://home-assistant.io/components/sensor.speedtest/
import logging
import re
import sys
from datetime import timedelta
from subprocess import check_output
import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import DOMAIN
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.helpers.event import track_time_change
REQUIREMENTS = ['speedtest-cli==0.3.4']
_LOGGER = logging.getLogger(__name__)
@@ -21,6 +22,7 @@ _SPEEDTEST_REGEX = re.compile(r'Ping:\s(\d+\.\d+)\sms[\r\n]+'
r'Upload:\s(\d+\.\d+)\sMbit/s[\r\n]+')
CONF_MONITORED_CONDITIONS = 'monitored_conditions'
CONF_SECOND = 'second'
CONF_MINUTE = 'minute'
CONF_HOUR = 'hour'
CONF_DAY = 'day'
@@ -30,13 +32,10 @@ SENSOR_TYPES = {
'upload': ['Upload', 'Mbit/s'],
}
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Speedtest sensor."""
data = SpeedtestData()
data = SpeedtestData(hass, config)
dev = []
for sensor in config[CONF_MONITORED_CONDITIONS]:
if sensor not in SENSOR_TYPES:
@@ -46,6 +45,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(dev)
def update(call=None):
"""Update service for manual updates."""
data.update(dt_util.now())
for sensor in dev:
sensor.update()
hass.services.register(DOMAIN, 'update_speedtest', update)
# pylint: disable=too-few-public-methods
class SpeedtestSensor(Entity):
@@ -76,7 +83,6 @@ class SpeedtestSensor(Entity):
def update(self):
"""Get the latest data and update the states."""
self.speedtest_client.update()
data = self.speedtest_client.data
if data is None:
return
@@ -92,12 +98,16 @@ class SpeedtestSensor(Entity):
class SpeedtestData(object):
"""Get the latest data from speedtest.net."""
def __init__(self):
def __init__(self, hass, config):
"""Initialize the data object."""
self.data = None
track_time_change(hass, self.update,
second=config.get(CONF_SECOND, 0),
minute=config.get(CONF_MINUTE, 0),
hour=config.get(CONF_HOUR, None),
day=config.get(CONF_DAY, None))
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
def update(self, now):
"""Get the latest data from speedtest.net."""
import speedtest_cli

View File

@@ -0,0 +1,61 @@
"""
Sensor for Supervisord process status.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.supervisord/
"""
import logging
import xmlrpc.client
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Supervisord platform."""
try:
supervisor_server = xmlrpc.client.ServerProxy(
config.get('url', 'http://localhost:9001/RPC2'))
except ConnectionRefusedError:
_LOGGER.error('Could not connect to Supervisord')
return
processes = supervisor_server.supervisor.getAllProcessInfo()
add_devices(
[SupervisorProcessSensor(info, supervisor_server)
for info in processes])
class SupervisorProcessSensor(Entity):
"""Represent a supervisor-monitored process."""
# pylint: disable=abstract-method
def __init__(self, info, server):
"""Initialize the sensor."""
self._info = info
self._server = server
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._info.get('name')
@property
def state(self):
"""Return the state of the sensor."""
return self._info.get('statename')
def update(self):
"""Update device state."""
self._info = self._server.supervisor.getProcessInfo(
self._info.get('name'))
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'group': self._info.get('group'),
'description': self._info.get('description')
}

6
homeassistant/components/sensor/systemmonitor.py Normal file → Executable file
View File

@@ -10,7 +10,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['psutil==4.1.0']
REQUIREMENTS = ['psutil==4.2.0']
SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'],
'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'],
@@ -24,9 +24,9 @@ SENSOR_TYPES = {
'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'],
'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'],
'network_out': ['Sent', 'MiB', 'mdi:server-network'],
'network_in': ['Recieved', 'MiB', 'mdi:server-network'],
'network_in': ['Received', 'MiB', 'mdi:server-network'],
'packets_out': ['Packets sent', '', 'mdi:server-network'],
'packets_in': ['Packets recieved', '', 'mdi:server-network'],
'packets_in': ['Packets received', '', 'mdi:server-network'],
'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
'last_boot': ['Last Boot', '', 'mdi:clock'],

View File

@@ -0,0 +1,34 @@
"""
Support for Qwikswitch relays.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.qwikswitch/
"""
import logging
import homeassistant.components.qwikswitch as qwikswitch
from homeassistant.components.switch import SwitchDevice
DEPENDENCIES = ['qwikswitch']
class QSSwitch(qwikswitch.QSToggleEntity, SwitchDevice):
"""Switch based on a Qwikswitch relay module."""
pass
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Store add_devices for the switch components."""
if discovery_info is None or 'qsusb_id' not in discovery_info:
logging.getLogger(__name__).error(
'Configure main Qwikswitch component')
return False
qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']]
for item in qsusb.ha_devices:
if item['type'] == 'switch':
dev = QSSwitch(item, qsusb)
add_devices([dev])
qsusb.ha_objects[item['id']] = dev

View File

@@ -14,7 +14,7 @@ REQUIREMENTS = ['rpi-rf==0.9.5']
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
# pylint: disable=unused-argument, import-error
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return switches controlled by a generic RF device via GPIO."""
import rpi_rf

View File

@@ -40,7 +40,6 @@ class Thermostat(ThermostatDevice):
self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index)
self._name = self.thermostat['name']
self._away = 'away' in self.thermostat['program']['currentClimateRef']
self.hold_temp = hold_temp
def update(self):
@@ -121,9 +120,7 @@ class Thermostat(ThermostatDevice):
@property
def mode(self):
"""Return current mode ie. home, away, sleep."""
mode = self.thermostat['program']['currentClimateRef']
self._away = 'away' in mode
return mode
return self.thermostat['program']['currentClimateRef']
@property
def hvac_mode(self):
@@ -144,11 +141,16 @@ class Thermostat(ThermostatDevice):
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away
mode = self.mode
events = self.thermostat['events']
for event in events:
if event['running']:
mode = event['holdClimateRef']
break
return 'away' in mode
def turn_away_mode_on(self):
"""Turn away on."""
self._away = True
if self.hold_temp:
self.data.ecobee.set_climate_hold(self.thermostat_index,
"away", "indefinite")
@@ -157,7 +159,6 @@ class Thermostat(ThermostatDevice):
def turn_away_mode_off(self):
"""Turn away off."""
self._away = False
self.data.ecobee.resume_program(self.thermostat_index)
def set_temperature(self, temperature):
@@ -180,20 +181,16 @@ class Thermostat(ThermostatDevice):
# def turn_home_mode_on(self):
# """ Turns home mode on. """
# self._away = False
# self.data.ecobee.set_climate_hold(self.thermostat_index, "home")
# def turn_home_mode_off(self):
# """ Turns home mode off. """
# self._away = False
# self.data.ecobee.resume_program(self.thermostat_index)
# def turn_sleep_mode_on(self):
# """ Turns sleep mode on. """
# self._away = False
# self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep")
# def turn_sleep_mode_off(self):
# """ Turns sleep mode off. """
# self._away = False
# self.data.ecobee.resume_program(self.thermostat_index)

View File

@@ -17,7 +17,7 @@ DEFAULT_NAME = 'ZWave Thermostat'
REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
WORKAROUND_IGNORE = 'ignore'
@@ -40,16 +40,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
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),
value.index)
int(value.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_IGNORE:
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat, ignoring")
return
else:
add_devices([ZWaveThermostat(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK)
add_devices([ZWaveThermostat(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments

View File

@@ -1,7 +1,7 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
__version__ = "0.19"
__version__ = "0.20.2"
REQUIRED_PYTHON_VER = (3, 4)
PLATFORM_FORMAT = '{}.{}'

View File

@@ -79,6 +79,7 @@ class HomeAssistant(object):
def restart_homeassistant(*args):
"""Reset Home Assistant."""
_LOGGER.warning('Home Assistant requested a restart.')
request_restart.set()
request_shutdown.set()
@@ -92,14 +93,21 @@ class HomeAssistant(object):
except ValueError:
_LOGGER.warning(
'Could not bind to SIGTERM. Are you running in a thread?')
while not request_shutdown.isSet():
try:
try:
signal.signal(signal.SIGHUP, restart_homeassistant)
except ValueError:
_LOGGER.warning(
'Could not bind to SIGHUP. Are you running in a thread?')
except AttributeError:
pass
try:
while not request_shutdown.isSet():
time.sleep(1)
except KeyboardInterrupt:
break
except KeyboardInterrupt:
pass
finally:
self.stop()
self.stop()
return RESTART_EXIT_CODE if request_restart.isSet() else 0
def stop(self):

View File

@@ -146,7 +146,7 @@ def time_period_str(value):
time_period = vol.Any(time_period_str, timedelta, time_period_dict)
def log_exception(logger, ex, domain):
def log_exception(logger, ex, domain, config):
"""Generate log exception for config validation."""
message = 'Invalid config for [{}]: '.format(domain)
if 'extra keys not allowed' in ex.error_message:
@@ -154,7 +154,12 @@ def log_exception(logger, ex, domain):
.format(ex.path[-1], domain, domain,
'->'.join('%s' % m for m in ex.path))
else:
message += ex.error_message
message += str(ex)
if hasattr(config, '__line__'):
message += " (See {}:{})".format(config.__config_file__,
config.__line__ or '?')
logger.error(message)
@@ -364,6 +369,8 @@ CONDITION_SCHEMA = vol.Any(
TEMPLATE_CONDITION_SCHEMA,
TIME_CONDITION_SCHEMA,
ZONE_CONDITION_SCHEMA,
AND_CONDITION_SCHEMA,
OR_CONDITION_SCHEMA,
)
_SCRIPT_DELAY_SCHEMA = vol.Schema({

View File

@@ -55,12 +55,23 @@ class EntityComponent(object):
self._setup_platform(p_type, p_config)
if self.discovery_platforms:
# Discovery listener for all items in discovery_platforms array
# passed from a component's setup method (e.g. light/__init__.py)
discovery.listen(
self.hass, self.discovery_platforms.keys(),
lambda service, info:
self._setup_platform(self.discovery_platforms[service], {},
info))
# Generic discovery listener for loading platform dynamically
# Refer to: homeassistant.components.discovery.load_platform()
def load_platform_callback(service, info):
"""Callback to load a platform."""
platform = info.pop(discovery.LOAD_PLATFORM)
self._setup_platform(platform, {}, info if info else None)
discovery.listen(self.hass, discovery.LOAD_PLATFORM + '.' +
self.domain, load_platform_callback)
def extract_from_service(self, service):
"""Extract all known entities from a service call.

View File

@@ -16,8 +16,8 @@ from homeassistant.components.thermostat import (
ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE,
SERVICE_SET_TEMPERATURE)
from homeassistant.components.hvac import (
ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION, ATTR_AUX_HEAT,
SERVICE_SET_HUMIDITY, SERVICE_SET_SWING,
ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION_MODE, ATTR_AUX_HEAT,
SERVICE_SET_HUMIDITY, SERVICE_SET_SWING_MODE,
SERVICE_SET_OPERATION_MODE, SERVICE_SET_AUX_HEAT)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY,
@@ -48,8 +48,8 @@ SERVICE_ATTRIBUTES = {
SERVICE_SET_FAN_MODE: [ATTR_FAN],
SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE],
SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY],
SERVICE_SET_SWING: [ATTR_SWING_MODE],
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION],
SERVICE_SET_SWING_MODE: [ATTR_SWING_MODE],
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE],
SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT],
SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE],
}

View File

@@ -57,6 +57,7 @@ def render(hass, template, variables=None, **kwargs):
'states': AllStates(hass),
'utcnow': utcnow,
'as_timestamp': dt_util.as_timestamp,
'relative_time': dt_util.get_age
}).render(kwargs).strip()
except jinja2.TemplateError as err:
raise TemplateError(err)

View File

@@ -1,48 +1,47 @@
"""Color util methods."""
import math
# pylint: disable=unused-import
from webcolors import html5_parse_legacy_color as color_name_to_rgb # noqa
HASS_COLOR_MAX = 500 # mireds (inverted)
HASS_COLOR_MIN = 154
# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py
# Taken from:
# http://www.developers.meethue.com/documentation/color-conversions-rgb-xy
# License: Code is given as is. Use at your own risk and discretion.
# pylint: disable=invalid-name
def color_RGB_to_xy(R, G, B):
"""Convert from RGB color to XY color."""
if R + G + B == 0:
return 0, 0
return 0, 0, 0
var_R = (R / 255.)
var_G = (G / 255.)
var_B = (B / 255.)
R = R / 255
B = B / 255
G = G / 255
if var_R > 0.04045:
var_R = ((var_R + 0.055) / 1.055) ** 2.4
else:
var_R /= 12.92
# Gamma correction
R = pow((R + 0.055) / (1.0 + 0.055),
2.4) if (R > 0.04045) else (R / 12.92)
G = pow((G + 0.055) / (1.0 + 0.055),
2.4) if (G > 0.04045) else (G / 12.92)
B = pow((B + 0.055) / (1.0 + 0.055),
2.4) if (B > 0.04045) else (B / 12.92)
if var_G > 0.04045:
var_G = ((var_G + 0.055) / 1.055) ** 2.4
else:
var_G /= 12.92
# Wide RGB D65 conversion formula
X = R * 0.664511 + G * 0.154324 + B * 0.162028
Y = R * 0.313881 + G * 0.668433 + B * 0.047685
Z = R * 0.000088 + G * 0.072310 + B * 0.986039
if var_B > 0.04045:
var_B = ((var_B + 0.055) / 1.055) ** 2.4
else:
var_B /= 12.92
# Convert XYZ to xy
x = X / (X + Y + Z)
y = Y / (X + Y + Z)
var_R *= 100
var_G *= 100
var_B *= 100
# Brightness
Y = 1 if Y > 1 else Y
brightness = round(Y * 255)
# Observer. = 2 deg, Illuminant = D65
X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
# Convert XYZ to xy, see CIE 1931 color space on wikipedia
return X / (X + Y + Z), Y / (X + Y + Z)
return round(x, 3), round(y, 3), brightness
# taken from

View File

@@ -152,3 +152,52 @@ def parse_time(time_str):
except ValueError:
# ValueError if value cannot be converted to an int or not in range
return None
# Found in this gist: https://gist.github.com/zhangsen/1199964
def get_age(date):
# pylint: disable=too-many-return-statements
"""
Take a datetime and return its "age" as a string.
The age can be in second, minute, hour, day, month or year. Only the
biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will
be returned.
Make sure date is not in the future, or else it won't work.
"""
def formatn(number, unit):
"""Add "unit" if it's plural."""
if number == 1:
return "1 %s" % unit
elif number > 1:
return "%d %ss" % (number, unit)
def q_n_r(first, second):
"""Return quotient and remaining."""
return first // second, first % second
delta = now() - date
day = delta.days
second = delta.seconds
year, day = q_n_r(day, 365)
if year > 0:
return formatn(year, 'year')
month, day = q_n_r(day, 30)
if month > 0:
return formatn(month, 'month')
if day > 0:
return formatn(day, 'day')
hour, second = q_n_r(second, 3600)
if hour > 0:
return formatn(hour, 'hour')
minute, second = q_n_r(second, 60)
if minute > 0:
return formatn(minute, 'minute')
if second > 0:
return formatn(second, 'second')
return "0 second"

View File

@@ -3,6 +3,7 @@ import logging
import os
from collections import OrderedDict
import glob
import yaml
from homeassistant.exceptions import HomeAssistantError
@@ -44,14 +45,55 @@ def _include_yaml(loader, node):
return load_yaml(fname)
def _include_dir_named_yaml(loader, node):
"""Load multiple files from dir as a dict."""
mapping = OrderedDict()
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
for fname in glob.glob(files):
filename = os.path.splitext(os.path.basename(fname))[0]
mapping[filename] = load_yaml(fname)
return mapping
def _include_dir_merge_named_yaml(loader, node):
"""Load multiple files from dir as a merged dict."""
mapping = OrderedDict()
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
for fname in glob.glob(files):
loaded_yaml = load_yaml(fname)
if isinstance(loaded_yaml, dict):
mapping.update(loaded_yaml)
return mapping
def _include_dir_list_yaml(loader, node):
"""Load multiple files from dir as a list."""
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
return [load_yaml(f) for f in glob.glob(files)]
def _include_dir_merge_list_yaml(loader, node):
"""Load multiple files from dir as a merged list."""
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
merged_list = []
for fname in glob.glob(files):
loaded_yaml = load_yaml(fname)
if isinstance(loaded_yaml, list):
merged_list.extend(loaded_yaml)
return merged_list
def _ordered_dict(loader, node):
"""Load YAML mappings into an ordered dict to preserve key order."""
loader.flatten_mapping(node)
nodes = loader.construct_pairs(node)
seen = {}
min_line = None
for (key, _), (node, _) in zip(nodes, node.value):
line = getattr(node, '__line__', 'unknown')
if line != 'unknown' and (min_line is None or line < min_line):
min_line = line
if key in seen:
fname = getattr(loader.stream, 'name', '')
first_mark = yaml.Mark(fname, 0, seen[key], -1, None, None)
@@ -62,7 +104,10 @@ def _ordered_dict(loader, node):
)
seen[key] = line
return OrderedDict(nodes)
processed = OrderedDict(nodes)
processed.__config_file__ = loader.name
processed.__line__ = min_line
return processed
def _env_var_yaml(loader, node):
@@ -78,3 +123,9 @@ yaml.SafeLoader.add_constructor('!include', _include_yaml)
yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
_ordered_dict)
yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml)
yaml.SafeLoader.add_constructor('!include_dir_list', _include_dir_list_yaml)
yaml.SafeLoader.add_constructor('!include_dir_merge_list',
_include_dir_merge_list_yaml)
yaml.SafeLoader.add_constructor('!include_dir_named', _include_dir_named_yaml)
yaml.SafeLoader.add_constructor('!include_dir_merge_named',
_include_dir_merge_named_yaml)

View File

@@ -6,6 +6,7 @@ pip>=7.0.0
vincenty==0.1.4
jinja2>=2.8
voluptuous==0.8.9
webcolors==1.5
# homeassistant.components.isy994
PyISY==1.0.5
@@ -37,6 +38,11 @@ blockchain==1.3.1
# homeassistant.components.thermostat.eq3btsmart
# bluepy_devices>=0.2.0
# homeassistant.components.notify.aws_lambda
# homeassistant.components.notify.aws_sns
# homeassistant.components.notify.aws_sqs
boto3==1.3.1
# homeassistant.components.notify.xmpp
dnspython3==1.12.0
@@ -97,6 +103,9 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl
# homeassistant.components.alarm_control_panel.alarmdotcom
https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1
# homeassistant.components.media_player.roku
https://github.com/bah2830/python-roku/archive/3.1.1.zip#python-roku==3.1.1
# homeassistant.components.modbus
https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0
@@ -109,8 +118,11 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9.
# homeassistant.components.sensor.sabnzbd
https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
# homeassistant.components.qwikswitch
https://github.com/kellerza/pyqwikswitch/archive/v0.3.zip#pyqwikswitch==0.3
# homeassistant.components.ecobee
https://github.com/nkgilley/python-ecobee-api/archive/92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4
https://github.com/nkgilley/python-ecobee-api/archive/4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5
# homeassistant.components.switch.edimax
https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1
@@ -119,7 +131,7 @@ https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f
https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3
# homeassistant.components.sensor.gtfs
https://github.com/robbiet480/pygtfs/archive/432414b720c580fb2667a0a48f539118a2d95969.zip#pygtfs==0.1.2
https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3
# homeassistant.components.scene.hunterdouglas_powerview
https://github.com/sander76/powerviewApi/archive/master.zip#powerviewApi==0.2
@@ -130,6 +142,9 @@ https://github.com/theolind/pymysensors/archive/cc5d0b325e13c2b623fa934f69eea7cd
# homeassistant.components.notify.googlevoice
https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#pygooglevoice-sms==0.0.1
# homeassistant.components.media_player.lg_netcast
https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0
# homeassistant.components.influxdb
influxdb==2.12.0
@@ -153,7 +168,7 @@ messagebird==1.2.0
mficlient==0.3.0
# homeassistant.components.discovery
netdisco==0.6.6
netdisco==0.6.7
# homeassistant.components.sensor.neurio_energy
neurio==0.2.10
@@ -168,6 +183,7 @@ paho-mqtt==1.1
panasonic_viera==0.2
# homeassistant.components.device_tracker.aruba
# homeassistant.components.device_tracker.asuswrt
pexpect==4.0.1
# homeassistant.components.light.hue
@@ -180,7 +196,7 @@ plexapi==1.1.0
proliphix==0.1.0
# homeassistant.components.sensor.systemmonitor
psutil==4.1.0
psutil==4.2.0
# homeassistant.components.notify.pushbullet
pushbullet.py==0.10.0
@@ -215,8 +231,11 @@ pyfttt==0.3
# homeassistant.components.device_tracker.icloud
pyicloud==0.8.3
# homeassistant.components.sensor.lastfm
pylast==1.6.0
# homeassistant.components.sensor.loopenergy
pyloopenergy==0.0.10
pyloopenergy==0.0.12
# homeassistant.components.device_tracker.netgear
pynetgear==0.3.3
@@ -241,7 +260,7 @@ python-forecastio==1.3.4
python-mpd2==0.5.5
# homeassistant.components.nest
python-nest==2.6.0
python-nest==2.9.2
# homeassistant.components.device_tracker.nmap_tracker
python-nmap==0.6.0
@@ -253,7 +272,7 @@ python-pushover==0.2
python-statsd==1.7.2
# homeassistant.components.notify.telegram
python-telegram-bot==4.0.1
python-telegram-bot==4.1.1
# homeassistant.components.sensor.twitch
python-twitch==1.2.0
@@ -280,7 +299,7 @@ pywemo==0.4.2
radiotherm==1.2
# homeassistant.components.switch.rpi_rf
rpi-rf==0.9.5
# rpi-rf==0.9.5
# homeassistant.components.media_player.yamaha
rxv==0.1.11
@@ -326,6 +345,9 @@ tellive-py==0.5.2
# homeassistant.components.switch.transmission
transmissionrpc==0.11
# homeassistant.components.notify.twilio_sms
twilio==5.4.0
# homeassistant.components.sensor.uber
uber_rides==0.2.1
@@ -344,6 +366,9 @@ vsure==0.8.1
# homeassistant.components.switch.wake_on_lan
wakeonlan==0.2.2
# homeassistant.components.media_player.gpmdp
websocket-client==0.35.0
# homeassistant.components.zigbee
xbee-helper==0.0.7

View File

@@ -1,4 +1,4 @@
flake8>=2.5.1
flake8>=2.5.4
pylint>=1.5.5
coveralls>=1.1
pytest>=2.9.1

View File

@@ -15,7 +15,7 @@ if [ -d python-openzwave ]; then
git pull --recurse-submodules=yes
git submodule update --init --recursive
else
git clone --recursive https://github.com/OpenZWave/python-openzwave.git
git clone --recursive --depth 1 https://github.com/OpenZWave/python-openzwave.git
cd python-openzwave
fi

View File

@@ -8,6 +8,7 @@ import sys
COMMENT_REQUIREMENTS = [
'RPi.GPIO',
'rpi-rf',
'Adafruit_Python_DHT',
'fritzconnection',
'pybluez',

View File

@@ -12,8 +12,9 @@ User=%i
# Enable the following line if you get network-related HA errors during boot
#ExecStartPre=/usr/bin/sleep 60
# Use `whereis hass` to determine the path of hass
ExecStart=/usr/bin/hass
ExecStart=/usr/bin/hass --runner
SendSIGKILL=no
RestartForceExitStatus=100
[Install]
WantedBy=multi-user.target

View File

@@ -18,6 +18,7 @@ REQUIRES = [
'vincenty==0.1.4',
'jinja2>=2.8',
'voluptuous==0.8.9',
'webcolors==1.5',
]
setup(

View File

@@ -55,6 +55,21 @@ LOCATION_MESSAGE_INACCURATE = {
'tst': 1,
'vel': 0}
LOCATION_MESSAGE_ZERO_ACCURACY = {
'batt': 92,
'cog': 248,
'tid': 'user',
'lon': 2.0,
't': 'u',
'alt': 27,
'acc': 0,
'p': 101.3977584838867,
'vac': 4,
'lat': 6.0,
'_type': 'location',
'tst': 1,
'vel': 0}
REGION_ENTER_MESSAGE = {
'lon': 1.0,
'event': 'enter',
@@ -204,6 +219,14 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase):
self.assert_location_latitude(2.0)
self.assert_location_longitude(1.0)
def test_location_zero_accuracy_gps(self):
"""Ignore the location for zero accuracy GPS information."""
self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE)
self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE_ZERO_ACCURACY)
self.assert_location_latitude(2.0)
self.assert_location_longitude(1.0)
def test_event_entry_exit(self):
"""Test the entry event."""
self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)
@@ -230,6 +253,20 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase):
# Left clean zone state
self.assertFalse(owntracks.REGIONS_ENTERED[USER])
def test_event_with_spaces(self):
"""Test the entry event."""
message = REGION_ENTER_MESSAGE.copy()
message['desc'] = "inner 2"
self.send_message(EVENT_TOPIC, message)
self.assert_location_state('inner_2')
message = REGION_LEAVE_MESSAGE.copy()
message['desc'] = "inner 2"
self.send_message(EVENT_TOPIC, message)
# Left clean zone state
self.assertFalse(owntracks.REGIONS_ENTERED[USER])
def test_event_entry_exit_inaccurate(self):
"""Test the event for inaccurate exit."""
self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)

View File

@@ -33,7 +33,7 @@ class TestDemoHvac(unittest.TestCase):
self.assertEqual(21, state.attributes.get('temperature'))
self.assertEqual('on', state.attributes.get('away_mode'))
self.assertEqual(22, state.attributes.get('current_temperature'))
self.assertEqual("On High", state.attributes.get('fan'))
self.assertEqual("On High", state.attributes.get('fan_mode'))
self.assertEqual(67, state.attributes.get('humidity'))
self.assertEqual(54, state.attributes.get('current_humidity'))
self.assertEqual("Off", state.attributes.get('swing_mode'))
@@ -81,17 +81,17 @@ class TestDemoHvac(unittest.TestCase):
def test_set_fan_mode_bad_attr(self):
"""Test setting fan mode without required attribute."""
state = self.hass.states.get(ENTITY_HVAC)
self.assertEqual("On High", state.attributes.get('fan'))
self.assertEqual("On High", state.attributes.get('fan_mode'))
hvac.set_fan_mode(self.hass, None, ENTITY_HVAC)
self.hass.pool.block_till_done()
self.assertEqual("On High", state.attributes.get('fan'))
self.assertEqual("On High", state.attributes.get('fan_mode'))
def test_set_fan_mode(self):
"""Test setting of new fan mode."""
hvac.set_fan_mode(self.hass, "On Low", ENTITY_HVAC)
self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_HVAC)
self.assertEqual("On Low", state.attributes.get('fan'))
self.assertEqual("On Low", state.attributes.get('fan_mode'))
def test_set_swing_mode_bad_attr(self):
"""Test setting swing mode without required attribute."""

View File

@@ -0,0 +1,131 @@
"""The tests for the MoldIndicator sensor."""
import unittest
import homeassistant.components.sensor as sensor
from homeassistant.components.sensor.mold_indicator import (ATTR_DEWPOINT,
ATTR_CRITICAL_TEMP)
from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS)
from tests.common import get_test_home_assistant
class TestSensorMoldIndicator(unittest.TestCase):
"""Test the MoldIndicator sensor."""
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.states.set('test.indoortemp', '20',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.states.set('test.outdoortemp', '10',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.states.set('test.indoorhumidity', '50',
{ATTR_UNIT_OF_MEASUREMENT: '%'})
self.hass.pool.block_till_done()
def tearDown(self):
"""Stop down everything that was started."""
self.hass.stop()
def test_setup(self):
"""Test the mold indicator sensor setup."""
self.assertTrue(sensor.setup(self.hass, {
'sensor': {
'platform': 'mold_indicator',
'indoor_temp_sensor': 'test.indoortemp',
'outdoor_temp_sensor': 'test.outdoortemp',
'indoor_humidity_sensor': 'test.indoorhumidity',
'calibration_factor': '2.0'
}
}))
moldind = self.hass.states.get('sensor.mold_indicator')
assert moldind
assert '%' == moldind.attributes.get('unit_of_measurement')
def test_invalidhum(self):
"""Test invalid sensor values."""
self.hass.states.set('test.indoortemp', '10',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.states.set('test.outdoortemp', '10',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.states.set('test.indoorhumidity', '0',
{ATTR_UNIT_OF_MEASUREMENT: '%'})
self.assertTrue(sensor.setup(self.hass, {
'sensor': {
'platform': 'mold_indicator',
'indoor_temp_sensor': 'test.indoortemp',
'outdoor_temp_sensor': 'test.outdoortemp',
'indoor_humidity_sensor': 'test.indoorhumidity',
'calibration_factor': '2.0'
}
}))
moldind = self.hass.states.get('sensor.mold_indicator')
assert moldind
# assert state
assert moldind.state == '0'
def test_calculation(self):
"""Test the mold indicator internal calculations."""
self.assertTrue(sensor.setup(self.hass, {
'sensor': {
'platform': 'mold_indicator',
'indoor_temp_sensor': 'test.indoortemp',
'outdoor_temp_sensor': 'test.outdoortemp',
'indoor_humidity_sensor': 'test.indoorhumidity',
'calibration_factor': '2.0'
}
}))
moldind = self.hass.states.get('sensor.mold_indicator')
assert moldind
# assert dewpoint
dewpoint = moldind.attributes.get(ATTR_DEWPOINT)
assert dewpoint
assert dewpoint > 9.25
assert dewpoint < 9.26
# assert temperature estimation
esttemp = moldind.attributes.get(ATTR_CRITICAL_TEMP)
assert esttemp
assert esttemp > 14.9
assert esttemp < 15.1
# assert mold indicator value
state = moldind.state
assert state
assert state == '68'
def test_sensor_changed(self):
"""Test the sensor_changed function."""
self.assertTrue(sensor.setup(self.hass, {
'sensor': {
'platform': 'mold_indicator',
'indoor_temp_sensor': 'test.indoortemp',
'outdoor_temp_sensor': 'test.outdoortemp',
'indoor_humidity_sensor': 'test.indoorhumidity',
'calibration_factor': '2.0'
}
}))
# Change indoor temp
self.hass.states.set('test.indoortemp', '30',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.pool.block_till_done()
assert self.hass.states.get('sensor.mold_indicator').state == '90'
# Change outdoor temp
self.hass.states.set('test.outdoortemp', '25',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.pool.block_till_done()
assert self.hass.states.get('sensor.mold_indicator').state == '57'
# Change humidity
self.hass.states.set('test.indoorhumidity', '20',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.pool.block_till_done()
assert self.hass.states.get('sensor.mold_indicator').state == '23'

View File

@@ -0,0 +1,88 @@
"""The tests for the Logentries component."""
import unittest
from unittest import mock
import homeassistant.components.logentries as logentries
from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED
class TestLogentries(unittest.TestCase):
"""Test the Logentries component."""
def test_setup_config_full(self):
"""Test setup with all data."""
config = {
'logentries': {
'host': 'host',
'token': 'secret',
}
}
hass = mock.MagicMock()
self.assertTrue(logentries.setup(hass, config))
self.assertTrue(hass.bus.listen.called)
self.assertEqual(EVENT_STATE_CHANGED,
hass.bus.listen.call_args_list[0][0][0])
def test_setup_config_defaults(self):
"""Test setup with defaults."""
config = {
'logentries': {
'host': 'host',
'token': 'token',
}
}
hass = mock.MagicMock()
self.assertTrue(logentries.setup(hass, config))
self.assertTrue(hass.bus.listen.called)
self.assertEqual(EVENT_STATE_CHANGED,
hass.bus.listen.call_args_list[0][0][0])
def _setup(self, mock_requests):
"""Test the setup."""
self.mock_post = mock_requests.post
self.mock_request_exception = Exception
mock_requests.exceptions.RequestException = self.mock_request_exception
config = {
'logentries': {
'host': 'https://webhook.logentries.com/noformat/logs/token',
'token': 'token'
}
}
self.hass = mock.MagicMock()
logentries.setup(self.hass, config)
self.handler_method = self.hass.bus.listen.call_args_list[0][0][1]
@mock.patch.object(logentries, 'requests')
@mock.patch('json.dumps')
def test_event_listener(self, mock_dump, mock_requests):
"""Test event listener."""
mock_dump.side_effect = lambda x: x
self._setup(mock_requests)
valid = {'1': 1,
'1.0': 1.0,
STATE_ON: 1,
STATE_OFF: 0,
'foo': 'foo'}
for in_, out in valid.items():
state = mock.MagicMock(state=in_,
domain='fake',
object_id='entity',
attributes={})
event = mock.MagicMock(data={'new_state': state},
time_fired=12345)
body = [{
'domain': 'fake',
'entity_id': 'entity',
'attributes': {},
'time': '12345',
'value': out,
}]
payload = {'host': 'https://webhook.logentries.com/noformat/'
'logs/token',
'event': body}
self.handler_method(event)
self.mock_post.assert_called_once_with(
payload['host'], data=payload, timeout=10)
self.mock_post.reset_mock()

View File

@@ -1,6 +1,8 @@
"""The tests for the Recorder component."""
# pylint: disable=too-many-public-methods,protected-access
import unittest
import time
import json
from unittest.mock import patch
from homeassistant.const import MATCH_ALL
@@ -10,7 +12,7 @@ from tests.common import get_test_home_assistant
class TestRecorder(unittest.TestCase):
"""Test the chromecast module."""
"""Test the recorder module."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
@@ -25,6 +27,52 @@ class TestRecorder(unittest.TestCase):
self.hass.stop()
recorder._INSTANCE.block_till_done()
def _add_test_states(self):
"""Add multiple states to the db for testing."""
now = int(time.time())
five_days_ago = now - (60*60*24*5)
attributes = {'test_attr': 5, 'test_attr_10': 'nice'}
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done()
for event_id in range(5):
if event_id < 3:
timestamp = five_days_ago
state = 'purgeme'
else:
timestamp = now
state = 'dontpurgeme'
recorder.query("INSERT INTO states ("
"entity_id, domain, state, attributes,"
"last_changed, last_updated, created,"
"utc_offset, event_id)"
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
('test.recorder2', 'sensor', state,
json.dumps(attributes), timestamp, timestamp,
timestamp, -18000, event_id + 1000))
def _add_test_events(self):
"""Add a few events for testing."""
now = int(time.time())
five_days_ago = now - (60*60*24*5)
event_data = {'test_attr': 5, 'test_attr_10': 'nice'}
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done()
for event_id in range(5):
if event_id < 2:
timestamp = five_days_ago
event_type = 'EVENT_TEST_PURGE'
else:
timestamp = now
event_type = 'EVENT_TEST'
recorder.query("INSERT INTO events"
"(event_type, event_data, origin, created,"
"time_fired, utc_offset)"
"VALUES (?, ?, ?, ?, ?, ?)",
(event_type, json.dumps(event_data), 'LOCAL',
timestamp, timestamp, -18000))
def test_saving_state(self):
"""Test saving and restoring a state."""
entity_id = 'test.recorder'
@@ -76,3 +124,56 @@ class TestRecorder(unittest.TestCase):
# Recorder uses SQLite and stores datetimes as integer unix timestamps
assert event.time_fired.replace(microsecond=0) == \
db_event.time_fired.replace(microsecond=0)
def test_purge_old_states(self):
"""Test deleting old states."""
self._add_test_states()
# make sure we start with 5 states
states = recorder.query_states('SELECT * FROM states')
self.assertEqual(len(states), 5)
# run purge_old_data()
recorder._INSTANCE.purge_days = 4
recorder._INSTANCE._purge_old_data()
# we should only have 2 states left after purging
states = recorder.query_states('SELECT * FROM states')
self.assertEqual(len(states), 2)
def test_purge_old_events(self):
"""Test deleting old events."""
self._add_test_events()
events = recorder.query_events('SELECT * FROM events WHERE '
'event_type LIKE "EVENT_TEST%"')
self.assertEqual(len(events), 5)
# run purge_old_data()
recorder._INSTANCE.purge_days = 4
recorder._INSTANCE._purge_old_data()
# now we should only have 3 events left
events = recorder.query_events('SELECT * FROM events WHERE '
'event_type LIKE "EVENT_TEST%"')
self.assertEqual(len(events), 3)
def test_purge_disabled(self):
"""Test leaving purge_days disabled."""
self._add_test_states()
self._add_test_events()
# make sure we start with 5 states and events
states = recorder.query_states('SELECT * FROM states')
events = recorder.query_events('SELECT * FROM events WHERE '
'event_type LIKE "EVENT_TEST%"')
self.assertEqual(len(states), 5)
self.assertEqual(len(events), 5)
# run purge_old_data()
recorder._INSTANCE.purge_days = None
recorder._INSTANCE._purge_old_data()
# we should have all of our states still
states = recorder.query_states('SELECT * FROM states')
events = recorder.query_events('SELECT * FROM events WHERE '
'event_type LIKE "EVENT_TEST%"')
self.assertEqual(len(states), 5)
self.assertEqual(len(events), 5)

View File

@@ -37,10 +37,10 @@ class TestRFXTRX(unittest.TestCase):
'automatic_add': True,
'devices': {}}}))
while len(rfxtrx.RFX_DEVICES) < 1:
while len(rfxtrx.RFX_DEVICES) < 2:
time.sleep(0.1)
self.assertEqual(len(rfxtrx.RFXOBJECT.sensors()), 1)
self.assertEqual(len(rfxtrx.RFXOBJECT.sensors()), 2)
def test_valid_config(self):
"""Test configuration."""

View File

@@ -9,16 +9,17 @@ class TestColorUtil(unittest.TestCase):
# pylint: disable=invalid-name
def test_color_RGB_to_xy(self):
"""Test color_RGB_to_xy."""
self.assertEqual((0, 0), color_util.color_RGB_to_xy(0, 0, 0))
self.assertEqual((0.3127159072215825, 0.3290014805066623),
self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy(0, 0, 0))
self.assertEqual((0.32, 0.336, 255),
color_util.color_RGB_to_xy(255, 255, 255))
self.assertEqual((0.15001662234042554, 0.060006648936170214),
self.assertEqual((0.136, 0.04, 12),
color_util.color_RGB_to_xy(0, 0, 255))
self.assertEqual((0.3, 0.6), color_util.color_RGB_to_xy(0, 255, 0))
self.assertEqual((0.172, 0.747, 170),
color_util.color_RGB_to_xy(0, 255, 0))
self.assertEqual((0.6400744994567747, 0.3299705106316933),
self.assertEqual((0.679, 0.321, 80),
color_util.color_RGB_to_xy(255, 0, 0))
def test_color_xy_brightness_to_RGB(self):

View File

@@ -133,3 +133,32 @@ class TestDateUtil(unittest.TestCase):
def test_parse_datetime_returns_none_for_incorrect_format(self):
"""Test parse_datetime returns None if incorrect format."""
self.assertIsNone(dt_util.parse_datetime("not a datetime string"))
def test_get_age(self):
"""Test get_age."""
diff = dt_util.now() - timedelta(seconds=0)
self.assertEqual(dt_util.get_age(diff), "0 second")
diff = dt_util.now() - timedelta(seconds=30)
self.assertEqual(dt_util.get_age(diff), "30 seconds")
diff = dt_util.now() - timedelta(minutes=5)
self.assertEqual(dt_util.get_age(diff), "5 minutes")
diff = dt_util.now() - timedelta(minutes=1)
self.assertEqual(dt_util.get_age(diff), "1 minute")
diff = dt_util.now() - timedelta(minutes=300)
self.assertEqual(dt_util.get_age(diff), "5 hours")
diff = dt_util.now() - timedelta(minutes=320)
self.assertEqual(dt_util.get_age(diff), "5 hours")
diff = dt_util.now() - timedelta(minutes=2*60*24)
self.assertEqual(dt_util.get_age(diff), "2 days")
diff = dt_util.now() - timedelta(minutes=32*60*24)
self.assertEqual(dt_util.get_age(diff), "1 month")
diff = dt_util.now() - timedelta(minutes=365*60*24)
self.assertEqual(dt_util.get_age(diff), "1 year")

View File

@@ -1,6 +1,8 @@
"""Test Home Assistant yaml loader."""
import io
import unittest
import os
import tempfile
from homeassistant.util import yaml
@@ -32,3 +34,104 @@ class TestYaml(unittest.TestCase):
pass
else:
assert 0
def test_enviroment_variable(self):
"""Test config file with enviroment variable."""
os.environ["PASSWORD"] = "secret_password"
conf = "password: !env_var PASSWORD"
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert doc['password'] == "secret_password"
del os.environ["PASSWORD"]
def test_invalid_enviroment_variable(self):
"""Test config file with no enviroment variable sat."""
conf = "password: !env_var PASSWORD"
try:
with io.StringIO(conf) as f:
yaml.yaml.safe_load(f)
except Exception:
pass
else:
assert 0
def test_include_yaml(self):
"""Test include yaml."""
with tempfile.NamedTemporaryFile() as include_file:
include_file.write(b"value")
include_file.seek(0)
conf = "key: !include {}".format(include_file.name)
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert doc["key"] == "value"
def test_include_dir_list(self):
"""Test include dir list yaml."""
with tempfile.TemporaryDirectory() as include_dir:
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_1.write(b"one")
file_1.close()
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_2.write(b"two")
file_2.close()
conf = "key: !include_dir_list {}".format(include_dir)
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert sorted(doc["key"]) == sorted(["one", "two"])
def test_include_dir_named(self):
"""Test include dir named yaml."""
with tempfile.TemporaryDirectory() as include_dir:
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_1.write(b"one")
file_1.close()
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_2.write(b"two")
file_2.close()
conf = "key: !include_dir_named {}".format(include_dir)
correct = {}
correct[os.path.splitext(os.path.basename(file_1.name))[0]] = "one"
correct[os.path.splitext(os.path.basename(file_2.name))[0]] = "two"
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert doc["key"] == correct
def test_include_dir_merge_list(self):
"""Test include dir merge list yaml."""
with tempfile.TemporaryDirectory() as include_dir:
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_1.write(b"- one")
file_1.close()
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_2.write(b"- two\n- three")
file_2.close()
conf = "key: !include_dir_merge_list {}".format(include_dir)
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert sorted(doc["key"]) == sorted(["one", "two", "three"])
def test_include_dir_merge_named(self):
"""Test include dir merge named yaml."""
with tempfile.TemporaryDirectory() as include_dir:
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_1.write(b"key1: one")
file_1.close()
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_2.write(b"key2: two\nkey3: three")
file_2.close()
conf = "key: !include_dir_merge_named {}".format(include_dir)
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert doc["key"] == {
"key1": "one",
"key2": "two",
"key3": "three"
}