Compare commits

...

198 Commits

Author SHA1 Message Date
Paulus Schoutsen a764683f3a Merge pull request #2583 from home-assistant/hotfix-24-1
Hotfix 24 1
2016-07-20 22:45:03 -07:00
Paulus Schoutsen 19cb1a954f Version bump to 0.24.1 2016-07-20 22:42:46 -07:00
Nathan Henrie 7a1e2de49f Don't overwrite the config directory (#2570)
Closes #2566

The `else` seems to have been an error and was overwriting a non-default config directory with the default location.
2016-07-20 22:42:30 -07:00
Fabian Affolter 89639822f1 Fix version 2016-07-17 00:25:49 +02:00
Fabian Affolter 8c44ecc4ba Update version 2016-07-17 00:20:41 +02:00
Fabian Affolter dc0f16c9dd Merge pull request #2509 from home-assistant/dev
0.24
2016-07-17 00:03:26 +02:00
Paulus Schoutsen 16c71ab207 Make sqlalchemy main dependency to help migration (#2536) 2016-07-16 11:39:44 -07:00
Johann Kellerman 06d70544bc Update rpi_gpio.py (#2530)
Should be pullup, since the sensor pulls to ground (at least the one on AndrewHilliday's site)

Or do we want this configurable?
2016-07-16 11:10:41 -07:00
Pascal Vizeli 1877906fdf small bugfix (#2532) 2016-07-16 11:06:36 -07:00
Fabian Affolter 95d033f1af Round output of wind speed and humidity (#2535) 2016-07-16 11:05:29 -07:00
Paulus Schoutsen 7cff107c17 Update frontend 2016-07-16 02:15:46 -07:00
Fabian Affolter 89972ed940 Add validation and switch python-mystrom (#2529) 2016-07-15 09:02:20 -07:00
Pascal Vizeli 6694f29918 add media_player/clear_playlist and line-in/tv support to sonos (#2527)
* add media_player/clear_playlist and line-in/tv support to sonos

* add support source radio

* fix bug

* print TV/Line-In as media_title

* implement universal player

* add to demo platform

* Update demo.py

Better handling for demo object

* add unit tests

* fix unit test
2016-07-15 09:00:41 -07:00
Fabian Affolter c1798dbe1f Catch ImportError (#2526) 2016-07-14 15:15:53 -07:00
William Scanlon 3246b58437 Support for Wink lock user codes (#2525) 2016-07-14 13:31:16 -07:00
Michaël Arnauts 63356fb5eb supported_media_commands should check for SERVICE_SELECT_SOURCE instead of SUPPORT_SELECT_SOURCE (#2482) 2016-07-14 11:14:49 -07:00
Paulus Schoutsen ef64e11b50 known devices yaml robustness (#2523) 2016-07-13 23:56:02 -07:00
Paulus Schoutsen e38b7d97d2 Update frontend 2016-07-13 23:05:40 -07:00
Paulus Schoutsen 8984a6b161 update frontend 2016-07-13 19:11:33 -07:00
Paulus Schoutsen 49b595e32e Update frontend 2016-07-13 19:05:25 -07:00
Johann Kellerman a60a342864 Logbook: Query databse as_utc(). dt: Use pytz's localize (#2521) 2016-07-13 18:45:55 -07:00
Paulus Schoutsen 88b3aa54a8 Update README.rst 2016-07-13 18:43:04 -07:00
Fabian Affolter a0c1c918b8 Switch to xmltodict and pass over missing temperature (fixes #2433) (#2463)
* Switch to xmltodict and pass over missing temperature (fixes #2433)

* Add guard clauses
2016-07-13 18:30:11 -07:00
Pascal Vizeli 675283c23e Merge pull request #2520 from pvizeli/Homematic_pro
homematic update to pyhomematic 0.1.9
2016-07-13 23:29:22 +02:00
Pascal Vizeli c023d1d656 homematic update to pyhomematic 0.1.9 2016-07-13 23:15:21 +02:00
John Arild Berentsen ce4891fe8e Fix node inclusion and exclusion. Also add secure inclusion. (#2519)
Fix node inclusion and exclusion.
2016-07-13 19:56:14 +02:00
John Arild Berentsen 82d98f5b89 Zwave Node attributes was missing from binary sensors. (#2516)
Fixes #2505
2016-07-13 18:01:59 +02:00
heytcass 2900855061 Update README.rst (#2517)
Editing for typos, clarifying.
2016-07-13 08:59:26 -07:00
Greg Dowling e31d4863c7 Merge pull request #2514 from home-assistant/bump_pyloopenergy
Bump pyloopenergy version.
2016-07-13 17:34:30 +02:00
Paulus Schoutsen af736a3e71 Update frontend (temp map solution) 2016-07-13 08:32:13 -07:00
Daniel Høyer Iversen 16feb1c55e Fix issue #2290 for rfxtrx (#2498)
* Fix issue #2290 for rfxtrx

* update tests for rfxtrx sensor

* Replace state_unkown with None in rfxtrx sensor

* Update test_rfxtrx.py
2016-07-13 07:46:11 -07:00
Fabian Affolter 497bc6ac0d Update docstrings (#2513) 2016-07-13 14:47:29 +02:00
pavoni cae8f8a006 Bump pyloopenergy version. 2016-07-13 13:21:17 +02:00
Fabian Affolter 82e992c63c Links docs (#2510)
* Add link to docs

* Fix link to docs

* Update docstrings

* Fix link
2016-07-13 11:10:31 +02:00
Paulus Schoutsen 3dcafafc6a Merge branch 'master' into dev
Conflicts:
	homeassistant/const.py
2016-07-12 22:31:54 -07:00
Fabian Affolter ebcda4076e Upgrade zeroconf to 0.17.6 (#2503) 2016-07-12 21:56:23 -07:00
Robbie Trencheny 011f82f9e3 Uber sensor now works with UberPool and has a bit cleaner logic. Also upgraded to latest version of the SDK and switched all single quotes to double quotes (#2507) 2016-07-12 21:52:21 -07:00
Pascal Vizeli 8ed2c8e6a4 add photo functionality to telegram (#2506)
* add photo functionality to telegram

* basic auth need password and username
2016-07-12 21:48:33 -07:00
Brent b9cadbecaa Allow device_tracker and sensor entity for google travel times (#2479)
* Allow owntracks entity for google travel times

* Added ability to use sensor state as location

* Added zone checks for google travel timesg

* Updated to use global constents and the location helper

* Fixed type in method name and removed redundant validation

* Changed domain condition to be a bit more elegant

* Updated to allow friendly name in any instance including the config

* Fixed bad python syntax and used helper methods
2016-07-12 21:46:11 -07:00
Dan e1db639317 add hvac mode support to radiotherm (#2442)
* add hvac mode support to radiotherm

off/cool/heat/auto modes are supported

* Moved STATE_AUTO to thermostat component, fix lint

Moved STATE_AUTO to thermostat platform. Fixed lint error.
2016-07-12 21:43:49 -07:00
rhooper beeae17cab Merge pull request #2489 from home-assistant/recorder-tests
Add more recorder tests
2016-07-12 11:48:22 -07:00
Paulus Schoutsen 8fcfb9136c Update frontend 2016-07-12 09:16:21 -07:00
Fabian Affolter 62c11dde17 Upgrade python-telegram-bot to 4.3.3 (#2504) 2016-07-12 17:51:11 +02:00
Nolan Gilley e58615b2a5 Join by joaoapps component & notify platform (#2315)
* initial support for Join notifier

add more functions for Join

* rename to joaoapps_join

add message default in schema

move api_key check

* move special join services to their own component

update coveragerc and requirements_all

add icon and smallicon
2016-07-12 08:10:33 -07:00
Fabian Affolter bef2f87ddc Docstrings (#2502)
* Update docstrings

* Update docstrings

* Add link to docs
2016-07-12 16:46:29 +02:00
John Arild Berentsen 45a8b74d7f Add missing sensor command_class into sensor component (#2501)
command_class_sensor_alarm was also missing from sensor component.
2016-07-12 15:40:55 +02:00
Daniel Høyer Iversen 09a4336bc5 Fix bug in rfxtrx for int device id (#2497) 2016-07-12 01:45:22 -07:00
Paulus Schoutsen 6d60287455 Update frontend 2016-07-12 00:10:05 -07:00
Paulus Schoutsen 6cb91e66c8 Update frontend 2016-07-11 22:07:34 -07:00
Keaton Taylor 2189516966 Clamp brightness between 0 and 255 (#2494)
* Clamp brightness between 0 and 255

Change to ensure that values over 255 supplied by the config will be
clamed to a max value of 255.

* Revert "Clamp brightness between 0 and 255"

This reverts commit c87238e8b5.

* Clamp brightness between 0 and 255

Change to ensure that values over 255 supplied by the config will be
clamed to a max value of 255.
2016-07-11 12:39:46 -07:00
Paulus Schoutsen 1738db9ccc Update models.py 2016-07-11 12:38:35 -07:00
Paulus Schoutsen e0dd5a8558 Tweak Recorder 2016-07-11 08:56:07 -07:00
John Arild Berentsen f4f2da5dc7 Missing command class for sensor (#2492) 2016-07-11 16:33:34 +02:00
Daniel Høyer Iversen 085d026ab6 Merge pull request #2487 from home-assistant/rfxtrx
Rfxtrx
2016-07-11 09:18:51 +02:00
Daniel 3b14189021 Make rfxtrx sensor not crash when unknown sensor is discovered 2016-07-11 08:59:14 +02:00
Daniel 6b9e1f3263 update rfxtrx to version 0.9 to support lighting4 2016-07-11 08:54:15 +02:00
Dan bde2f0d5a0 Imap sensor (#2485)
* Imap unread email sensor

Checks the inbox of a imap account for unread emails. Tested against
gmail.

Example config:

```
sensor:
  - platform: imap
    name: gmail test
    user: USER
    password: PASSWORD
    server: imap.gmail.com
    port: 993

```

* added to .coveragerc

* Code cleanup and typo fix.

* Added port range validation

* Fix lint errors
2016-07-10 13:21:53 -07:00
Daniel Matuschek 50ea3c7744 Implementation of a KNX platform driver and a KNX switch (#2439)
* Implementation of a KNX platform driver and a KNX switch

* Starting working on a KNX thermostat implementation

* Removed KNX thermostat implementation from this branch again

* Make gateway parameter optional (can be auto-detected in many cases)

* Removed check for double initialisation

* KNX messages now will be handled internally and not send to the Home Assistant message bus

* Call update_ha_state only if should_poll is false

* Removed unused HASS variable

* knxip library version changed

* pylint optimization
2016-07-10 10:36:54 -07:00
Fabian Affolter bde9e4e9c0 Upgrade googlemaps to 2.4.4 (#2481) 2016-07-10 10:32:38 -07:00
GadgetReactor 609458052c New Switch Platform: TPLink Switch (HS100 / HS110) (#2453)
* New Switch Platform: TPLink Switch (HS100 / HS110)

### Information

The TPLink switch platform allows you to control the state of your TPLink Wi-Fi Smart Plugs.

Supported devices (tested):
HS100 (UK)

It should also work with the HS110.

To use your D-Link smart plugs in your installation, add the following to your configuration.yaml file:

"""
# Example configuration.yaml entry
switch:
  platform: tplink
  host: IP_ADRRESS
  name: TPLink Switch
"""

### Configuration variables:

host (Required): The IP address of your TPlink plug, eg. http://192.168.1.105
name (Optional): The name to use when displaying this switch.

* Update tplink.py

Bug fixes

* Separate to a standalone library

* Removed unnecessary imports

* Code cleanup and update reference library link

* TPLink switch support (#2453)

* updated requirements
2016-07-10 09:48:02 -07:00
clach04 344fb9c8b4 Fix typos in demo switch doc strings (#2480) 2016-07-09 09:35:55 +02:00
koen01 03ef74b4ab Add 'Sound' to rfxtrx DATA_TYPES (#2477)
Fixes reception of SelectPlus and correctly adds the chime sensor.
2016-07-08 09:00:21 -07:00
Dale Higgs ab63fbff3f Fix AsusWRT to prevent SSH key confusion (#2467)
Changed "pub_key" to "ssh_key" while maintaining backwards compatibility. Quotes were also updated to match across the file.
2016-07-08 08:58:31 -07:00
Pascal Vizeli 2ab2f68318 Yahoo! weather support (#2457)
* initial import yahoo weather

* fix temperature in HA style

* add suggestion from @fabaff

* change with suggestion from @balloob
2016-07-08 08:48:38 -07:00
John Arild Berentsen 5d6c13c12c Fix missing generic command class for binary sensors (#2475) 2016-07-08 13:40:04 +02:00
Brent ff5c3c9f98 Added attributes to the statsd data (#2440)
* Added attributes to the statsd data

* Updated to allow optional attribute logging
2016-07-07 23:09:02 -07:00
Paulus Schoutsen 31b8e49ad2 Fix PyLint 1.6 issues (#2471) 2016-07-07 18:54:16 -07:00
Neil Lathwood 978ebb9c59 Updated braviatv media player to support power status (#2470)
* Updated braviatv media player to support power status

* Updated requirements_all.txt
2016-07-07 18:28:01 -07:00
Johann Kellerman 85e3dfe6a6 Exclude secrets.yaml in yaml !include_directories (#2450) 2016-07-06 22:17:02 -07:00
Marcelo Moreira de Mello - mmello cf5aeebba6 - Added code validation on Simplisafe module on alarm_control_panel (#2455) 2016-07-06 21:55:47 -07:00
John Arild Berentsen 3e3d9c881e Return name of location to lock instead of serial number. (#2460) 2016-07-06 18:33:58 -07:00
Fabian Affolter 216a756590 Upgrade pyowm to 2.3.2 (fixes #2452) (#2464) 2016-07-06 18:31:11 -07:00
Dale Higgs db23320659 Add names, units and icons to APCUPSd Sensor (#2443)
* Add names, units and icons to APCUPSd Sensor

* Fix farcy errors

* Attempt fix of errors

* Remove "type:" from configuration

* Remove duplicate "mdi:" prefix
2016-07-06 18:25:57 -07:00
Fabian Affolter c634cbf866 Upgrade slacker to 0.9.21 (#2458) 2016-07-06 16:48:58 +02:00
Fabian Affolter ceb332bc31 Upgrade python-telegram-bot to 4.3.2 (#2459) 2016-07-06 16:48:43 +02:00
Dale Higgs 86e3fdee1c Fix flood of errors if Plex server goes offline (#2447) 2016-07-05 10:50:43 -07:00
Fabian Affolter 0f4acb59fe Change schema for elevation to int (#2436) 2016-07-05 08:01:59 -07:00
Paulus Schoutsen c5b2df01d9 Update frontend 2016-07-04 10:40:43 -07:00
Jordan Keith 83a72ab4dc Update unifi.py to support sites (#2434)
* Update unifi.py

Add support for a site that is not the default within the Unifi Controller.

i.e. A controller with multiple sites:

 - Home
 - Friends
 - Parents (default)

Supplying the identifier for 'Home' now means that the devices tracked will be associated with 'Home'.

* Update test_unifi.py

Fix test modules as well.
2016-07-04 08:20:00 -07:00
Johann Kellerman 2cdef7fb2f Persistent_notification service description (#2407)
* Persistent_notification service description

* Add service name to services.yaml
2016-07-03 18:33:23 -07:00
Paulus Schoutsen 659d67f362 properly cleanup after config test 2016-07-03 18:24:17 -07:00
Brent ffccca1f60 Updated to new statsd library and added state change counters (#2429) 2016-07-03 15:21:18 -07:00
Brent ef74bd9892 Updated to version 3.1.2 and fixed invalid host setup error (#2431) 2016-07-03 15:17:08 -07:00
Paulus Schoutsen 3447fdc76f Make scripts available via CLI (#2426)
* Rename sqlalchemy migrate script

* Add script support to CLI
2016-07-03 11:38:14 -07:00
rhooper a2e45b8fdd Switch to SQLAlchemy for the Recorder component. Gives the ability t… (#2377)
* Switch to SQLAlchemy for the Recorder component.  Gives the ability to use MySQL or other.

* fixes for failed lint

* add conversion script

* code review fixes and refactor to use to_native() model methods and execute() helper

* move script to homeassistant.scripts module

* style fixes my tox lint/flake8 missed

* move exclusion up
2016-07-02 11:22:51 -07:00
Fabian Affolter a65f196d19 Use XML source instead of website (#2400) 2016-07-02 11:22:29 -07:00
William Scanlon a74cdc7b0d SimpliSafe Alarm (#2409) 2016-07-02 11:21:15 -07:00
rhooper 449be29022 support newer deCONZ api versions (#2410) 2016-07-02 11:18:54 -07:00
Fabian Affolter ba8e417390 Upgrade python-telegram-bot to 4.3.1 (#2414) 2016-07-02 11:16:14 -07:00
Fabian Affolter cad995a5f4 Upgrade slacker to 0.9.18 (#2415) 2016-07-02 11:15:39 -07:00
Fabian Affolter 06efee7ecf Upgrade fuzzywuzzy to 0.11.0 (#2416) 2016-07-02 11:12:48 -07:00
Paulus Schoutsen bacc14d845 Merge pull request #2421 from armills/zwave-color-bulbs
Move Aeotec bulb color logic to Zwave workaround
2016-07-02 11:11:44 -07:00
Paulus Schoutsen 6f8a733434 Merge pull request #2424 from home-assistant/hotfix-23-1
Hotfix 0.23.1
2016-07-02 10:20:57 -07:00
Paulus Schoutsen 906e64fdb5 Bump version to 0.23.1 2016-07-02 10:06:24 -07:00
William Scanlon 8e406a70f6 Downgraded pubnub version (#2420) 2016-07-02 10:06:09 -07:00
AlucardZero 8d9f4a1754 check for OP_NO_COMPRESSION support before trying to use it (#2423) 2016-07-02 10:06:09 -07:00
rhooper 0a53b863cd bump pyvera version to 0.2.13 (#2406) 2016-07-02 10:06:09 -07:00
Fabian Affolter 80feb322f9 A mini update (#2418) 2016-07-02 10:05:19 -07:00
William Scanlon 2b514139eb Downgraded pubnub version (#2420) 2016-07-02 10:04:51 -07:00
AlucardZero 2b8dfb2a0e check for OP_NO_COMPRESSION support before trying to use it (#2423) 2016-07-02 10:03:49 -07:00
Adam Mills 6477122b23 Move Aeotec bulb color logic to Zwave workaround
Default behavior for warm/cold white channels is to assume the white
channel is mixed with the rgb. This is a sane default and should support
the Fibaro RGBW LED controller.
2016-07-02 12:08:01 -04:00
Fabian Affolter 1e9db41028 Remove unused links (#2417) 2016-07-02 15:06:13 +02:00
Fabian Affolter 21d3be4027 Fix update (#2402) 2016-07-02 09:09:22 +02:00
rhooper 48b3c98646 bump pyvera version to 0.2.13 (#2406) 2016-07-01 18:47:55 -07:00
Fabian Affolter 15803d1773 Move content to devel docs (fixes #2403) (#2408) 2016-07-02 00:47:54 +02:00
Fabian Affolter 3870d2e0cd Docstring updates (#2404)
* Fix docstring

* Fix typo

* Update docstrings

* Update docstrings
2016-07-01 21:39:30 +02:00
Paulus Schoutsen fe0164b137 Version bump to 0.24.0.dev0 2016-07-01 00:58:29 -07:00
Paulus Schoutsen 6bc504bfcc Merge pull request #2381 from home-assistant/dev
0.23
2016-07-01 00:58:11 -07:00
Paulus Schoutsen c44eefacb4 Version bump to 0.23.0 2016-07-01 00:57:55 -07:00
patkap 952b1a3e0c kodi platform: following jsonrpc-request version bump (0.3), let kodi file abstraction layer handle a collection item, url or file to play (#2398) 2016-06-30 16:35:20 -07:00
Pascal Vizeli a57cd58675 Merge pull request #2399 from pvizeli/Homematic_fix
Homematic fix
2016-07-01 00:17:04 +02:00
Pascal Vizeli d67f79e2eb remove unused pylint exeption 2016-07-01 00:01:16 +02:00
Pascal Vizeli d326d187d1 fix bug in event handling and add cast for watersensor 2016-06-30 23:54:04 +02:00
Pascal Vizeli d0b1619946 Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into Homematic_fix 2016-06-30 23:44:27 +02:00
Lewis Juggins 21be4c1828 Add Sonos unjoin functionality (#2379) 2016-06-30 14:21:57 -07:00
Paulus Schoutsen d1f4901d53 Migrate to cherrypy wsgi from eventlet (#2387) 2016-06-30 09:02:12 -07:00
patkap 7582eb9f63 jsonrpc-request version bump (0.3) (#2397) 2016-06-30 08:40:01 -07:00
Fabian Affolter 419ff18afb Docstrings (#2395)
* Replace switch with lock

* Update docstrings

* Add link to docs

* Add link to docs and update docstrings

* Update docstring

* Update docstrings and fix typos

* Add link to docs

* Add link to docs

* Add link to docs and update docstrings

* Fix link to docs and update docstrings

* Remove blank line

* Add link to docs
2016-06-30 10:33:34 +02:00
Fabian Affolter 8dd7ebb08e Add the two next trains (#2390) 2016-06-29 17:44:35 -07:00
rhooper 5cce02ab62 vera lock support (#2391)
* vera lock support

* fix formatting
2016-06-29 17:28:20 -07:00
William Scanlon 6a816116ab Wink subscription support (#2324) 2016-06-29 14:16:53 -07:00
Pascal Vizeli bb0f484caf update pyhomematic and homematic use now events from HA for remotes 2016-06-29 22:42:35 +02:00
Brent 3c5c018e3e Fixed issue with roku timeouts throwing exceptions when roku losses n… (#2386)
* Fixed issue with roku timeouts throwing exceptions when roku losses networking

* Fixed pylint errors
2016-06-28 20:26:37 -07:00
Ardetus 78e7e17484 Support more types of 1wire sensors and bus masters (#2384)
* Support more types of 1wire sensors and bus masters

- Added support for DS18S20, DS1822, DS1825 and DS28EA00 temperature sensors
- Added support for bus masters which use fuse to mount device tree.
  Mount can be specified by 'mount_dir' configuration parameter.

* Correct the lint problem
2016-06-28 18:39:16 -07:00
AlucardZero 31d2a5d2d1 Reenable TLS1.1 and 1.2 while leaving SSLv3 disabled (#2385) 2016-06-28 16:48:25 -07:00
Pascal Vizeli baa9bdf6fc change homematic to autodetect only 2016-06-28 22:53:53 +02:00
Fabian Affolter 00179763ef Upgrade influxdb to 3.0.0 (#2383) 2016-06-28 07:56:14 -07:00
Fabian Affolter 7a73dc7d6a Upgrade websocket-client to 0.37.0 (#2382) 2016-06-27 23:47:35 -07:00
Paulus Schoutsen d0b9b588a9 Merge branch 'master' into dev
Conflicts:
	homeassistant/const.py
2016-06-27 23:26:46 -07:00
Fabian Affolter 592c599488 Upgrade Werkzeug to 0.11.10 (#2380) 2016-06-27 17:39:44 -07:00
Paulus Schoutsen 6714392e9c Move elevation to core config and clean up HTTP mocking in tests (#2378)
* Stick version numbers

* Move elevation to core config

* Migrate forecast test to requests-mock

* Migrate YR tests to requests-mock

* Add requests_mock to requirements_test.txt

* Move conf code from bootstrap to config

* More config fixes

* Fix some more issues

* Add test for set config and failing auto detect
2016-06-27 09:02:45 -07:00
Adam Mills dc75b28b90 Initial Support for Zwave color bulbs (#2376)
* Initial Support for Zwave color bulbs

* Revert name override for ZwaveColorLight
2016-06-27 09:01:41 -07:00
Pascal Vizeli d2509ce9e3 Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into dev 2016-06-27 15:57:01 +02:00
Pascal Vizeli 3afc566be1 Fix timing bug while linking HM device to HA object
https://github.com/danielperna84/home-assistant/issues/14
2016-06-26 23:18:18 +02:00
Dan fb3e388f04 Depreciate ssl2/3 (#2375)
* Depreciate ssl2/3

Following the best practices as defind here:
https://mozilla.github.io/server-side-tls/ssl-config-generator/

* Updated comment with better decription

Links to the rational rather than the config generator; explains link.

* add comment mentioning intermediate
2016-06-26 11:49:46 -07:00
Fabian Affolter 254b1c46ac Remove lxml dependency (#2374) 2016-06-26 10:13:52 -07:00
Philip Lundrigan d13cc227cc Push State (#2365)
* Add ability to push state changes

* Add tests for push state changes

* Fix style issues

* Use better name to force an update
2016-06-26 00:33:23 -07:00
Paulus Schoutsen 446f998759 Merge pull request #2368 from pvizeli/Homematic
Homematic Support (clean)
2016-06-25 20:37:53 -07:00
Paulus Schoutsen 206e7d7a67 Extend persistent notification support (#2371) 2016-06-25 16:40:33 -07:00
Pascal Vizeli c3b25f2cd5 fix logging-not-lazy 2016-06-25 22:20:09 +02:00
Pascal Vizeli f3199e7dae fix wrong import 2016-06-25 22:13:29 +02:00
Pascal Vizeli 4ecd724578 fix linter errors 2016-06-25 22:10:47 +02:00
Pascal Vizeli e4d3b25f1e Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into Homematic
# Conflicts:
#	homeassistant/components/thermostat/homematic.py
2016-06-25 22:02:14 +02:00
Pascal Vizeli 7e7f7b64e5 Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into dev 2016-06-25 21:58:34 +02:00
Pascal Vizeli e0e9d3c57b change autodiscovery 2016-06-25 21:37:51 +02:00
Pascal Vizeli a687bdb388 Revert "Third batch of (minor) fixes as suggested by @balloob"
This reverts commit 87c138c559.
2016-06-25 21:03:41 +02:00
Pascal Vizeli 199fbc7a15 Revert "fix autodiscovery"
This reverts commit 86ccf26a1a.
2016-06-25 21:03:37 +02:00
Pascal Vizeli 57754cd2ff Revert "fix discovery function"
This reverts commit be72b04855.
2016-06-25 21:03:33 +02:00
John Arild Berentsen 21381a95d4 Zwave fixes. (#2373)
* Fix move_up and move_down

I managed to switch up the zwave move_up and move_down commands.
This PR fixes it.
Thank you @nunofgs for bringing this to my attention :)

* Fix for aeotec 6 multisensor
2016-06-25 20:35:36 +02:00
Pascal Vizeli be72b04855 fix discovery function 2016-06-25 20:30:02 +02:00
Pascal Vizeli 86ccf26a1a fix autodiscovery 2016-06-25 20:12:49 +02:00
Pascal Vizeli 87c138c559 Third batch of (minor) fixes as suggested by @balloob 2016-06-25 19:25:59 +02:00
Pascal Vizeli b3acd7d21d add resolvenames function support from pyhomematic (homegear only) 2016-06-25 18:54:14 +02:00
Pascal Vizeli a19f7bff28 fix false autodetect with HM GongSensor types 2016-06-25 18:36:52 +02:00
Pascal Vizeli 30b7c6b694 Second batch of (minor) fixes as suggested by @balloob 2016-06-25 18:34:35 +02:00
Daniel Perna 43faeff42a Moved trx/except, added debug messages, minor fixes 2016-06-25 18:19:05 +02:00
Daniel Perna 5ca26fc13f Moved try/except-block and moved delay to link_homematic 2016-06-25 16:25:33 +02:00
Daniel Perna 04748e3ad1 First batch of (minor) fixes as suggested by @balloob 2016-06-25 15:10:19 +02:00
Johann Kellerman 7b02dc434a Secrets support for configuration files (#2312)
* ! secret based on yaml.py

* Private Secrets Dict, removed cmdline, fixed log level

* Secrets limited to yaml only

* Add keyring & debug tests
2016-06-25 00:10:03 -07:00
Matthew Treinish 1c1d18053b Add cmus media device (#2321)
This commit adds support for the cmus console music player as a media
device.
2016-06-25 00:06:36 -07:00
arsaboo 2ac752d67a Add OpenExchangeRates sensor (#2356)
* Create openexchangerates.py

* Create OpenExchangeRates Sensor

* Add openexchangerate sensor

* Update openexchangerates.py

* Added params dict

* Update openexchangerates.py

* Update openexchangerates.py

* Update openexchangerates.py

* Update openexchangerates.py

* Added API key validation

* Update openexchangerates.py
2016-06-25 00:02:28 -07:00
John Arild Berentsen a1ef1c996c Fix physical manual update of state of device (#2372) 2016-06-24 23:22:14 -07:00
Paulus Schoutsen cbb897b2cf Update frontend 2016-06-24 22:34:55 -07:00
Fabian Affolter e4b67c9574 Add persistent notification component (#1844) 2016-06-24 21:43:44 -07:00
Daniel Høyer Iversen 7a8c5a0709 Add frontend to the example config (#2367) 2016-06-24 21:40:02 -07:00
Paulus Schoutsen aadd730ddd Merge branch 'pr/2348' into dev
Conflicts:
	.coveragerc
2016-06-24 21:30:08 -07:00
Paulus Schoutsen 68df3deee0 ABC consistent not implemented behavior (#2359) 2016-06-24 21:27:40 -07:00
Johann Kellerman c616115419 rpi_gpi garage_door controller (#2369) 2016-06-24 21:22:10 -07:00
Daniel Perna dfe1b8d934 Fixed minor feature-detection bug with incomplet configuration 2016-06-24 19:46:42 +02:00
John Arild Berentsen ec8dc25c9c Zwave garagedoor (#2361)
* First go at zwave Garage door

* Refactor of zwave discovery

* Allaround fixes for rollershutter and garage door
2016-06-24 11:44:24 -04:00
Pascal Vizeli 67a04c2a0e Initial clean import 2016-06-24 10:06:58 +02:00
Dale Higgs 600a3e3965 Allow service data to be passed to shell_command (#2362) 2016-06-23 08:47:56 -07:00
Fabian Affolter 3349bdc2bd Log successful and failed login attempts (#2347) 2016-06-23 12:34:13 +02:00
Dan Cinnamon 12e26d25a5 Bump to pyenvisalink 1.0 (#2358) 2016-06-22 22:48:16 -07:00
Matthew Treinish aa3d0e1047 Fix incorrect check on presence of password and pub_key (#2355)
This commit fixes an issue with the use of None in default values
for the config get() calls in __init__() of AsusWrtDeviceScanner.
These values are cast as strings and when a NoneType is cast it
returns the string "None" this broke the check for the existence
of these fields. This commit fixes the issue by changing the default
value to be an empty string '' which will conform with the behavior
expected by the ssh login code.

Closes #2343
2016-06-22 17:01:39 -07:00
happyleaves d0ee8abcb8 couple fixes 2016-06-22 17:29:22 -04:00
happyleaves 94b47d8bc3 addressed review 2016-06-22 17:07:46 -04:00
Fabian Affolter 7b942243ab Increase interval (#2353) 2016-06-22 20:12:36 +02:00
Paulus Schoutsen a70f922a71 ps - add reload core config service (#2350) 2016-06-22 09:13:18 -07:00
Jean-Philippe Bouillot 9ce9b8debb Add support for wind, battery, radio signals for Netatmo sensor (#2351)
* Add support for wind, battery, radio signals

* Fix indentation error

* second indentation fix

* Fix for pylint too many statements error

* Moving "pylint: disable=too-many-statements"
2016-06-22 09:01:53 -07:00
Dale Higgs d7b006600e [notify.pushover] Fix 'NoneType' error on data retrieval (#2352)
* Fix 'NoneType' error on data retrieval

* Reduce code for empty dict as the default
2016-06-22 08:54:44 -07:00
Paulus Schoutsen a564fe8286 Fix error log (#2349) 2016-06-21 22:26:40 -07:00
happyleaves 7fc9fa4b0c satisfy farcy 2016-06-21 19:31:40 -04:00
happyleavesaoc d87e969671 add cec platform 2016-06-21 18:36:34 -04:00
Fabian Affolter 278514b994 Add support for Fixer.io (#2336)
* Add support for Fixer.io

* Add unit of measurment and set throttle to one day
2016-06-21 07:43:02 -07:00
Fabian Affolter 38b0336694 Upgrade paho-mqtt to 1.2 (#2339) 2016-06-20 21:51:50 -07:00
Fabian Affolter caa096ebd5 Upgrade psutil to 4.3.0 (#2342)
* Upgrade psutil to 4.3.0

* Remove period
2016-06-20 21:51:07 -07:00
Fabian Affolter ba417a730b Upgrade slacker to 0.9.17 (#2340) 2016-06-20 08:55:57 -07:00
dale3h 6fa095f4a7 Add additional Pushover parameters (#2309)
* Add additional Pushover parameters

Add support for more Pushover parameters: target (device), sound, url, url_title, priority, timestamp

* Remove data dictionary reference

https://github.com/home-assistant/home-assistant/pull/2309#discussion_r67603127
2016-06-19 23:08:30 -07:00
John Arild Berentsen 5efa076080 Make sure we exit loop when value is set (#2326) 2016-06-19 22:42:23 -07:00
Antonio Párraga Navarro cbc0833360 Support for Sony Bravia TV (#2243)
* Added Sony Bravia support to HA

* Improvements to make it work on my poor raspberry 1

* Just a typo

* A few fixes in order to pass pylint

* - Remove noqa: was due to the 80 characters max per line restriction
- Move communication logic to a separate library at https://github.com/aparraga/braviarc.git
- Added dependency and adapt the code according to that

* A few improvements

* Just a typo in a comment

* Rebase from HM/dev

* Update requirements by executing the script/gen_requirements_all.py

* More isolation level for braviarc lib

* Remove unnecessary StringIO usage

* Revert submodule polymer commit

* Small refactorization and clean up of unused functions

* Executed script/gen_requirements_all.py

* Added a missing condition to ensure that a map is not null

* Fix missing parameter detected by pylint

* A few improvements, also added an empty line to avoid the lint error

* A typo
2016-06-19 22:35:26 -07:00
John Arild Berentsen 2e62053629 Basic implementation of Zwave Rollershutters (#2313)
* Basic implementation of Zwave Rollershutters

* Better filtering, by @wokar

* Fix typo

* Remove polling from component, and loop fix

* linter fix

* Filter to channel devices to correct component

* Remove overwriting of parent node name
2016-06-19 22:30:57 -07:00
Paulus Schoutsen 4f09279524 Merge pull request #2334 from home-assistant/hotfix-22-1
Hotfix 0.22.1
2016-06-19 21:16:22 -07:00
Paulus Schoutsen 57dfce1583 Version bump to 0.22.1 2016-06-19 20:55:21 -07:00
Paulus Schoutsen 33bafb8451 fix insteon hub discovery 2016-06-19 20:54:22 -07:00
Paulus Schoutsen f59e242c63 fix insteon hub discovery 2016-06-19 20:53:56 -07:00
Dan Cinnamon cb6f50b7ff Envisalink support (#2304)
* Created a new platform for envisalink-based alarm panels (Honeywell/DSC)

* Added a sensor component and cleanup

* Completed initial development.

* Fixing pylint issues.

* Fix more pylint issues

* Fixed more validation issues.

* Final pylint issues

* Final tweaks prior to PR.

* Fixed final pylint issue

* Resolved a few minor issues, and used volumptous for validation.

* Fixing final lint issues

* Fixes to validation schema and refactoring.
2016-06-19 10:45:07 -07:00
Paulus Schoutsen 44177a7fde Version bump to 0.23.0.dev0 2016-06-18 13:21:04 -07:00
215 changed files with 9455 additions and 2489 deletions
+23
View File
@@ -3,6 +3,7 @@ source = homeassistant
omit =
homeassistant/__main__.py
homeassistant/scripts/*.py
# omit pieces of code that rely on external devices being present
homeassistant/components/apcupsd.py
@@ -20,6 +21,9 @@ omit =
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py
homeassistant/components/insteon_hub.py
homeassistant/components/*/insteon_hub.py
@@ -81,8 +85,16 @@ omit =
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/knx.py
homeassistant/components/switch/knx.py
homeassistant/components/binary_sensor/knx.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
@@ -111,7 +123,10 @@ omit =
homeassistant/components/downloader.py
homeassistant/components/feedreader.py
homeassistant/components/garage_door/wink.py
homeassistant/components/garage_door/rpi_gpio.py
homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py
homeassistant/components/light/blinksticklight.py
homeassistant/components/light/hue.py
@@ -120,7 +135,9 @@ omit =
homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py
homeassistant/components/lirc.py
homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py
@@ -146,6 +163,7 @@ omit =
homeassistant/components/notify/gntp.py
homeassistant/components/notify/googlevoice.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
@@ -170,16 +188,19 @@ omit =
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/onewire.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/openexchangerates.py
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/sabnzbd.py
@@ -197,6 +218,7 @@ omit =
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/worldclock.py
homeassistant/components/sensor/yweather.py
homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/arest.py
homeassistant/components/switch/dlink.py
@@ -208,6 +230,7 @@ omit =
homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py
homeassistant/components/thermostat/eq3btsmart.py
+1 -4
View File
@@ -15,7 +15,7 @@
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:
If code communicates with devices, web services, or a:
- [ ] 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]).
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
@@ -26,8 +26,5 @@ If the code does not interact with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] Tests have been added to verify that the new code works.
[fork]: http://stackoverflow.com/a/7244456
[squash]: https://github.com/ginatrapani/todo.txt-android/wiki/Squash-All-Commits-Related-to-a-Single-Issue-into-a-Single-Commit
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L16
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51
+1 -75
View File
@@ -9,79 +9,5 @@ The process is straight-forward.
- Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
Still interested? Then you should read the next sections and get more details.
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
## Adding support for a new device
For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/).
After you finish adding support for your device:
- Check that all dependencies are included via the `REQUIREMENTS` variable in your platform/component and only imported inside functions that use them.
- Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`.
- Update the `.coveragerc` file to exclude your platform if there are no tests available or your new code uses a 3rd party library for communication with the device/service/sensor.
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/home-assistant/home-assistant.io).
- Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `tox` or `script/lint`.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
- Check for comments and suggestions on your Pull Request and keep an eye on the [CI output](https://travis-ci.org/home-assistant/home-assistant/).
If you add a platform for an existing component, there is usually no need for updating the frontend. Only if you've added a new component that should show up in the frontend, there are more steps needed:
- Update the file [`home-assistant-icons.html`](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html) with an icon for your domain ([pick one from this list](https://www.polymer-project.org/1.0/components/core-elements/demo.html#core-icon)).
- Update the demo component with two states that it provides.
- Add your component to `home-assistant.conf.example`.
Since you've updated `home-assistant-icons.html`, you've made changes to the frontend:
- Run `script/build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit.
### Setting states
It is the responsibility of the component to maintain the states of the devices in your domain. Each device should be a single state and, if possible, a group should be provided that tracks the combined state of the devices.
A state can have several attributes that will help the frontend in displaying your state:
- `friendly_name`: this name will be used as the name of the device
- `entity_picture`: this picture will be shown instead of the domain icon
- `unit_of_measurement`: this will be appended to the state in the interface
- `hidden`: This is a suggestion to the frontend on if the state should be hidden
These attributes are defined in [homeassistant.components](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/__init__.py#L25).
### Proper Visibility Handling
Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/helpers/entity.py) class. If this is done, visibility will be handled for you.
You can set a suggestion for your entity's visibility by setting the hidden property by doing something similar to the following.
```python
self.hidden = True
```
This will SUGGEST that the active frontend hides the entity. This requires that the active frontend support hidden cards (the default frontend does) and that the value of hidden be included in your attributes dictionary (see above). The Entity abstract class will take care of this for you.
Remember: The suggestion set by your component's code will always be overwritten by user settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa).
### Working on the frontend
The frontend is composed of [Polymer](https://www.polymer-project.org) web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the *http-component* in your config.
When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works.
## Testing your code
To test your code before submission, used the `tox` tool.
```bash
> pip install -U tox
> tox
```
This will run unit tests against python 3.4 and 3.5 (if both are available locally), as well as run a set of tests which validate `pep8` and `pylint` style of the code.
You can optionally run tests on only one tox target using the `-e` option to select an environment.
For instance `tox -e lint` will run the linters only, `tox -e py34` will run unit tests only on python 3.4.
### Notes on PyLint and PEP8 validation
In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change.
+6 -6
View File
@@ -18,7 +18,7 @@ tutorials and documentation.
|screenshot-states|
Examples of devices it can interface it:
Examples of devices Home Assistant can interface with:
- Monitoring connected devices to a wireless router:
`OpenWrt <https://openwrt.org/>`__,
@@ -61,11 +61,11 @@ Examples of devices it can interface it:
- `See full list of supported
devices <https://home-assistant.io/components/>`__
Built home automation on top of your devices:
Build home automation on top of your devices:
- Keep a precise history of every change to the state of your house
- Turn on the lights when people get home after sun set
- Turn on lights slowly during sun set to compensate for less light
- Turn on the lights when people get home after sunset
- Turn on lights slowly during sunset to compensate for less light
- Turn off all lights and devices when everybody leaves the house
- Offers a `REST API <https://home-assistant.io/developers/api/>`__
and can interface with MQTT for easy integration with other projects
@@ -75,10 +75,10 @@ Built home automation on top of your devices:
(NMA) <http://www.notifymyandroid.com/>`__,
`PushBullet <https://www.pushbullet.com/>`__,
`PushOver <https://pushover.net/>`__, `Slack <https://slack.com/>`__,
`Telegram <https://telegram.org/>`__, and `Jabber
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
(XMPP) <http://xmpp.org>`__
The system is built modular so support for other devices or actions can
The system is built using a modular approach so support for other devices or actions can
be implemented easily. See also the `section on
architecture <https://home-assistant.io/developers/architecture/>`__
and the `section on creating your own
+34 -40
View File
@@ -7,6 +7,9 @@ homeassistant:
latitude: 32.87336
longitude: 117.22743
# Impacts weather/sunrise data
elevation: 665
# C for Celsius, F for Fahrenheit
temperature_unit: C
@@ -22,6 +25,9 @@ http:
# Set to 1 to enable development mode
# development: 1
# Enable the frontend
frontend:
light:
# platform: hue
@@ -30,17 +36,12 @@ wink:
access_token: 'YOUR_TOKEN'
device_tracker:
# The following types are available: ddwrt, netgear, tomato, luci,
# and nmap_tracker
# The following tracker are available:
# https://home-assistant.io/components/#presence-detection
platform: netgear
host: 192.168.1.1
username: admin
password: PASSWORD
# http_id is needed for Tomato routers only
# http_id: ABCDEFGHH
# For nmap_tracker, only the IP addresses to scan are needed:
# hosts: 192.168.1.1/24 # netmask prefix notation or
# hosts: 192.168.1.1-255 # address range
chromecast:
@@ -71,24 +72,25 @@ device_sun_light_trigger:
# A comma separated list of states that have to be tracked as a single group
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
# You can also have groups within groups.
# https://home-assistant.io/components/group/
group:
Home:
- group.living_room
- group.kitchen
living_room:
- light.Bowl
- light.Ceiling
- light.TV_back_light
kitchen:
- light.fan_bulb_1
- light.fan_bulb_2
children:
- device_tracker.child_1
- device_tracker.child_2
default_view:
view: yes
entities:
- group.awesome_people
- group.climate
process:
# items are which processes to look for: <entity_id>: <search string within ps>
xbmc: XBMC.App
kitchen:
name: Kitchen
entities:
- switch.kitchen_pin_3
upstairs:
name: Kids
icon: mdi:account-multiple
view: yes
entities:
- input_boolean.notify_home
- camera.demo_camera
example:
@@ -102,6 +104,7 @@ browser:
keyboard:
# https://home-assistant.io/getting-started/automation/
automation:
- alias: 'Rule 1 Light on in the evening'
trigger:
@@ -123,7 +126,6 @@ automation:
entity_id: group.living_room
- alias: 'Rule 2 - Away Mode'
trigger:
- platform: state
entity_id: group.all_devices
@@ -136,6 +138,14 @@ automation:
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
# Each sensor label should be unique or your sensors might not load correctly.
# Another way to do is to collect all entries under one "sensor:"
# sensor:
# - platform: mqtt
# name: "MQTT Sensor 1"
# - platform: mqtt
# name: "MQTT Sensor 2"
#
# Details: https://home-assistant.io/getting-started/devices/
sensor:
platform: systemmonitor
@@ -146,14 +156,6 @@ sensor:
arg: '/home'
- type: 'disk_use'
arg: '/home'
- type: 'disk_free'
arg: '/'
- type: 'memory_use_percent'
- type: 'memory_use'
- type: 'memory_free'
- type: 'processor_use'
- type: 'process'
arg: 'octave-cli'
sensor 2:
platform: forecast
@@ -163,14 +165,6 @@ sensor 2:
- precip_type
- precip_intensity
- temperature
- dew_point
- wind_speed
- wind_bearing
- cloud_cover
- humidity
- pressure
- visibility
- ozone
script:
# Turns on the bedroom lights and then the living room lights 1 minute later
+8 -67
View File
@@ -7,7 +7,6 @@ import platform
import subprocess
import sys
import threading
import time
from homeassistant.const import (
__version__,
@@ -110,22 +109,14 @@ def get_arguments():
type=int,
default=None,
help='Enables daily log rotation and keeps up to the specified days')
parser.add_argument(
'--install-osx',
action='store_true',
help='Installs as a service on OS X and loads on boot.')
parser.add_argument(
'--uninstall-osx',
action='store_true',
help='Uninstalls from OS X.')
parser.add_argument(
'--restart-osx',
action='store_true',
help='Restarts on OS X.')
parser.add_argument(
'--runner',
action='store_true',
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
parser.add_argument(
'--script',
nargs=argparse.REMAINDER,
help='Run one of the embedded scripts')
if os.name == "posix":
parser.add_argument(
'--daemon',
@@ -196,46 +187,6 @@ def write_pid(pid_file):
sys.exit(1)
def install_osx():
"""Setup to run via launchd on OS X."""
with os.popen('which hass') as inp:
hass_path = inp.read().strip()
with os.popen('whoami') as inp:
user = inp.read().strip()
cwd = os.path.dirname(__file__)
template_path = os.path.join(cwd, 'startup', 'launchd.plist')
with open(template_path, 'r', encoding='utf-8') as inp:
plist = inp.read()
plist = plist.replace("$HASS_PATH$", hass_path)
plist = plist.replace("$USER$", user)
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
try:
with open(path, 'w', encoding='utf-8') as outp:
outp.write(plist)
except IOError as err:
print('Unable to write to ' + path, err)
return
os.popen('launchctl load -w -F ' + path)
print("Home Assistant has been installed. \
Open it here: http://localhost:8123")
def uninstall_osx():
"""Unload from launchd on OS X."""
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
os.popen('launchctl unload ' + path)
print("Home Assistant has been uninstalled.")
def closefds_osx(min_fd, max_fd):
"""Make sure file descriptors get closed when we restart.
@@ -358,23 +309,13 @@ def main():
args = get_arguments()
if args.script is not None:
from homeassistant import scripts
return scripts.run(args.script)
config_dir = os.path.join(os.getcwd(), args.config)
ensure_config_path(config_dir)
# OS X launchd functions
if args.install_osx:
install_osx()
return 0
if args.uninstall_osx:
uninstall_osx()
return 0
if args.restart_osx:
uninstall_osx()
# A small delay is needed on some systems to let the unload finish.
time.sleep(0.5)
install_osx()
return 0
# Daemon functions
if args.pid_file:
check_pid(args.pid_file)
+16 -117
View File
@@ -3,7 +3,6 @@
import logging
import logging.handlers
import os
import shutil
import sys
from collections import defaultdict
from threading import RLock
@@ -11,22 +10,16 @@ from threading import RLock
import voluptuous as vol
import homeassistant.components as core_components
import homeassistant.components.group as group
import homeassistant.config as config_util
from homeassistant.components import group, persistent_notification
import homeassistant.config as conf_util
import homeassistant.core as core
import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
import homeassistant.util.dt as date_util
import homeassistant.util.location as loc_util
import homeassistant.util.package as pkg_util
from homeassistant.const import (
CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME,
CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED,
TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__)
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
event_decorators, service, config_per_platform, extract_domain_configs)
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_SETUP_LOCK = RLock()
@@ -208,11 +201,6 @@ def prepare_setup_platform(hass, config, domain, platform_name):
return platform
def mount_local_lib_path(config_dir):
"""Add local library to Python Path."""
sys.path.insert(0, os.path.join(config_dir, 'deps'))
# 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, skip_pip=False,
@@ -226,18 +214,17 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
if config_dir is not None:
config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir
mount_local_lib_path(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(
core_config))
except vol.MultipleInvalid as ex:
conf_util.process_ha_core_config(hass, core_config)
except vol.Invalid as ex:
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
return None
process_ha_config_upgrade(hass)
conf_util.process_ha_config_upgrade(hass)
if enable_log:
enable_logging(hass, verbose, log_rotate_days)
@@ -262,9 +249,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
if not core_components.setup(hass, config):
_LOGGER.error('Home Assistant core failed to initialize. '
'Further initialization aborted.')
return hass
persistent_notification.setup(hass, config)
_LOGGER.info('Home Assistant core initialized')
# Give event decorators access to HASS
@@ -291,12 +279,12 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
# Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
_mount_local_lib_path(config_dir)
enable_logging(hass, verbose, log_rotate_days)
try:
config_dict = config_util.load_yaml_config_file(config_path)
config_dict = conf_util.load_yaml_config_file(config_path)
except HomeAssistantError:
return None
@@ -355,101 +343,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
'Unable to setup error log %s (access denied)', err_log_path)
def process_ha_config_upgrade(hass):
"""Upgrade config if necessary."""
version_path = hass.config.path('.HA_VERSION')
try:
with open(version_path, 'rt') as inp:
conf_version = inp.readline().strip()
except FileNotFoundError:
# Last version to not have this file
conf_version = '0.7.7'
if conf_version == __version__:
return
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
__version__)
# This was where dependencies were installed before v0.18
# Probably should keep this around until ~v0.20.
lib_path = hass.config.path('lib')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
lib_path = hass.config.path('deps')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
with open(version_path, 'wt') as outp:
outp.write(__version__)
def process_ha_core_config(hass, config):
"""Process the [homeassistant] section from the config."""
hac = hass.config
def set_time_zone(time_zone_str):
"""Helper method to set time zone."""
if time_zone_str is None:
return
time_zone = date_util.get_time_zone(time_zone_str)
if time_zone:
hac.time_zone = time_zone
date_util.set_default_time_zone(time_zone)
else:
_LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name')):
if key in config:
setattr(hac, attr, config[key])
if CONF_TIME_ZONE in config:
set_time_zone(config.get(CONF_TIME_ZONE))
for entity_id, attrs in config.get(CONF_CUSTOMIZE).items():
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
if CONF_TEMPERATURE_UNIT in config:
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
# If we miss some of the needed values, auto detect them
if None not in (
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
return
_LOGGER.warning('Incomplete core config. Auto detecting location and '
'temperature unit')
info = loc_util.detect_location_info()
if info is None:
_LOGGER.error('Could not detect location information')
return
if hac.latitude is None and hac.longitude is None:
hac.latitude = info.latitude
hac.longitude = info.longitude
if hac.temperature_unit is None:
if info.use_fahrenheit:
hac.temperature_unit = TEMP_FAHRENHEIT
else:
hac.temperature_unit = TEMP_CELSIUS
if hac.location_name is None:
hac.location_name = info.city
if hac.time_zone is None:
set_time_zone(info.time_zone)
def _ensure_loader_prepared(hass):
"""Ensure Home Assistant loader is prepared."""
if not loader.PREPARED:
loader.prepare(hass)
def _mount_local_lib_path(config_dir):
"""Add local library to Python Path."""
sys.path.insert(0, os.path.join(config_dir, 'deps'))
+24
View File
@@ -19,6 +19,8 @@ from homeassistant.const import (
_LOGGER = logging.getLogger(__name__)
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
def is_on(hass, entity_id=None):
"""Load up the module to call the is_on method.
@@ -73,6 +75,11 @@ def toggle(hass, entity_id=None, **service_data):
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
def reload_core_config(hass):
"""Reload the core config."""
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
def setup(hass, config):
"""Setup general services related to Home Assistant."""
def handle_turn_service(service):
@@ -111,4 +118,21 @@ def setup(hass, config):
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
def handle_reload_config(call):
"""Service handler for reloading core config."""
from homeassistant.exceptions import HomeAssistantError
from homeassistant import config as conf_util
try:
path = conf_util.find_config_file(hass.config.config_dir)
conf = conf_util.load_yaml_config_file(path)
except HomeAssistantError as err:
_LOGGER.error(err)
return
conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
handle_reload_config)
return True
@@ -0,0 +1,105 @@
"""
Support for Envisalink-based alarm control panels (Honeywell/DSC).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.envisalink/
"""
import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.envisalink import (EVL_CONTROLLER,
EnvisalinkDevice,
PARTITION_SCHEMA,
CONF_CODE,
CONF_PARTITIONNAME,
SIGNAL_PARTITION_UPDATE,
SIGNAL_KEYPAD_UPDATE)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
DEPENDENCIES = ['envisalink']
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Perform the setup for Envisalink alarm panels."""
_configured_partitions = discovery_info['partitions']
_code = discovery_info[CONF_CODE]
for part_num in _configured_partitions:
_device_config_data = PARTITION_SCHEMA(
_configured_partitions[part_num])
_device = EnvisalinkAlarm(
part_num,
_device_config_data[CONF_PARTITIONNAME],
_code,
EVL_CONTROLLER.alarm_state['partition'][part_num],
EVL_CONTROLLER)
add_devices_callback([_device])
return True
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Represents the Envisalink-based alarm panel."""
# pylint: disable=too-many-arguments
def __init__(self, partition_number, alarm_name, code, info, controller):
"""Initialize the alarm panel."""
from pydispatch import dispatcher
self._partition_number = partition_number
self._code = code
_LOGGER.debug('Setting up alarm: ' + alarm_name)
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
dispatcher.connect(self._update_callback,
signal=SIGNAL_PARTITION_UPDATE,
sender=dispatcher.Any)
dispatcher.connect(self._update_callback,
signal=SIGNAL_KEYPAD_UPDATE,
sender=dispatcher.Any)
def _update_callback(self, partition):
"""Update HA state, if needed."""
if partition is None or int(partition) == self._partition_number:
self.update_ha_state()
@property
def code_format(self):
"""The characters if code is defined."""
return self._code
@property
def state(self):
"""Return the state of the device."""
if self._info['status']['alarm']:
return STATE_ALARM_TRIGGERED
elif self._info['status']['armed_away']:
return STATE_ALARM_ARMED_AWAY
elif self._info['status']['armed_stay']:
return STATE_ALARM_ARMED_HOME
elif self._info['status']['alpha']:
return STATE_ALARM_DISARMED
else:
return STATE_UNKNOWN
def alarm_disarm(self, code=None):
"""Send disarm command."""
if self._code:
EVL_CONTROLLER.disarm_partition(str(code),
self._partition_number)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if self._code:
EVL_CONTROLLER.arm_stay_partition(str(code),
self._partition_number)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if self._code:
EVL_CONTROLLER.arm_away_partition(str(code),
self._partition_number)
def alarm_trigger(self, code=None):
"""Alarm trigger command. Not possible for us."""
raise NotImplementedError()
@@ -0,0 +1,124 @@
"""
Interfaces with SimpliSafe alarm control panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.simplisafe/
"""
import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN,
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/w1ll1am23/simplisafe-python/archive/'
'586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#'
'simplisafe-python==0.0.1']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the SimpliSafe platform."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is None or password is None:
_LOGGER.error('Must specify username and password!')
return False
add_devices([SimpliSafeAlarm(
config.get('name', "SimpliSafe"),
username,
password,
config.get('code'))])
# pylint: disable=abstract-method
class SimpliSafeAlarm(alarm.AlarmControlPanel):
"""Representation a SimpliSafe alarm."""
def __init__(self, name, username, password, code):
"""Initialize the SimpliSafe alarm."""
from simplisafe import SimpliSafe
self.simplisafe = SimpliSafe(username, password)
self._name = name
self._code = str(code) if code else None
self._id = self.simplisafe.get_id()
status = self.simplisafe.get_state()
if status == 'Off':
self._state = STATE_ALARM_DISARMED
elif status == 'Home':
self._state = STATE_ALARM_ARMED_HOME
elif status == 'Away':
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = STATE_UNKNOWN
@property
def should_poll(self):
"""Poll the SimpliSafe API."""
return True
@property
def name(self):
"""Return the name of the device."""
if self._name is not None:
return self._name
else:
return 'Alarm {}'.format(self._id)
@property
def code_format(self):
"""One or more characters if code is defined."""
return None if self._code is None else '.+'
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Update alarm status."""
self.simplisafe.get_location()
status = self.simplisafe.get_state()
if status == 'Off':
self._state = STATE_ALARM_DISARMED
elif status == 'Home':
self._state = STATE_ALARM_ARMED_HOME
elif status == 'Away':
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = STATE_UNKNOWN
def alarm_disarm(self, code=None):
"""Send disarm command."""
if not self._validate_code(code, 'disarming'):
return
self.simplisafe.set_state('off')
_LOGGER.info('SimpliSafe alarm disarming')
self.update()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if not self._validate_code(code, 'arming home'):
return
self.simplisafe.set_state('home')
_LOGGER.info('SimpliSafe alarm arming home')
self.update()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if not self._validate_code(code, 'arming away'):
return
self.simplisafe.set_state('away')
_LOGGER.info('SimpliSafe alarm arming away')
self.update()
def _validate_code(self, code, state):
"""Validate given code."""
check = self._code is None or code == self._code
if not check:
_LOGGER.warning('Wrong code entered for %s', state)
return check
+13 -27
View File
@@ -6,7 +6,7 @@ https://home-assistant.io/developers/api/
"""
import json
import logging
from time import time
import queue
import homeassistant.core as ha
import homeassistant.remote as rem
@@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView):
def get(self, request):
"""Provide a streaming interface for the event bus."""
from eventlet.queue import LightQueue, Empty
import eventlet
cur_hub = eventlet.hubs.get_hub()
request.environ['eventlet.minimum_write_chunk_size'] = 0
to_write = LightQueue()
stop_obj = object()
to_write = queue.Queue()
restrict = request.args.get('restrict')
if restrict:
restrict = restrict.split(',')
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
def thread_forward_events(event):
def forward_events(event):
"""Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED:
return
@@ -99,28 +94,20 @@ class APIEventStream(HomeAssistantView):
else:
data = json.dumps(event, cls=rem.JSONEncoder)
cur_hub.schedule_call_global(0, lambda: to_write.put(data))
to_write.put(data)
def stream():
"""Stream events to response."""
self.hass.bus.listen(MATCH_ALL, thread_forward_events)
self.hass.bus.listen(MATCH_ALL, forward_events)
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
last_msg = time()
# Fire off one message right away to have browsers fire open event
to_write.put(STREAM_PING_PAYLOAD)
while True:
try:
# Somehow our queue.get sometimes takes too long to
# be notified of arrival of data. Probably
# because of our spawning on hub in other thread
# hack. Because current goal is to get this out,
# We just timeout every second because it will
# return right away if qsize() > 0.
# So yes, we're basically polling :(
payload = to_write.get(timeout=1)
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
if payload is stop_obj:
break
@@ -129,15 +116,13 @@ class APIEventStream(HomeAssistantView):
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip())
yield msg.encode("UTF-8")
last_msg = time()
except Empty:
if time() - last_msg > 50:
to_write.put(STREAM_PING_PAYLOAD)
except queue.Empty:
to_write.put(STREAM_PING_PAYLOAD)
except GeneratorExit:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
break
self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events)
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
self.hass.bus.remove_listener(MATCH_ALL, forward_events)
return self.Response(stream(), mimetype='text/event-stream')
@@ -204,11 +189,12 @@ class APIEntityStateView(HomeAssistantView):
return self.json_message('No state specified', HTTP_BAD_REQUEST)
attributes = request.json.get('attributes')
force_update = request.json.get('force_update', False)
is_new_state = self.hass.states.get(entity_id) is None
# Write state
self.hass.states.set(entity_id, new_state, attributes)
self.hass.states.set(entity_id, new_state, attributes, force_update)
# Read the state back for our response
resp = self.json(self.hass.states.get(entity_id))
@@ -0,0 +1,71 @@
"""
Support for Envisalink zone states- represented as binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.envisalink/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.envisalink import (EVL_CONTROLLER,
ZONE_SCHEMA,
CONF_ZONENAME,
CONF_ZONETYPE,
EnvisalinkDevice,
SIGNAL_ZONE_UPDATE)
from homeassistant.const import ATTR_LAST_TRIP_TIME
DEPENDENCIES = ['envisalink']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup Envisalink binary sensor devices."""
_configured_zones = discovery_info['zones']
for zone_num in _configured_zones:
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num])
_device = EnvisalinkBinarySensor(
zone_num,
_device_config_data[CONF_ZONENAME],
_device_config_data[CONF_ZONETYPE],
EVL_CONTROLLER.alarm_state['zone'][zone_num],
EVL_CONTROLLER)
add_devices_callback([_device])
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
"""Representation of an Envisalink binary sensor."""
# pylint: disable=too-many-arguments
def __init__(self, zone_number, zone_name, zone_type, info, controller):
"""Initialize the binary_sensor."""
from pydispatch import dispatcher
self._zone_type = zone_type
self._zone_number = zone_number
_LOGGER.debug('Setting up zone: ' + zone_name)
EnvisalinkDevice.__init__(self, zone_name, info, controller)
dispatcher.connect(self._update_callback,
signal=SIGNAL_ZONE_UPDATE,
sender=dispatcher.Any)
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
return attr
@property
def is_on(self):
"""Return true if sensor is on."""
return self._info['status']['open']
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return self._zone_type
def _update_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self.update_ha_state()
@@ -0,0 +1,100 @@
"""
Support for Homematic binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.homematic/
"""
import logging
from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.components.homematic as homematic
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematic']
SENSOR_TYPES_CLASS = {
"Remote": None,
"ShutterContact": "opening",
"Smoke": "smoke",
"SmokeV2": "smoke",
"Motion": "motion",
"MotionV2": "motion",
"RemoteMotion": None
}
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the Homematic binary sensor platform."""
if discovery_info is None:
return
return homematic.setup_hmdevice_discovery_helper(HMBinarySensor,
discovery_info,
add_callback_devices)
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
"""Representation of a binary Homematic device."""
@property
def is_on(self):
"""Return true if switch is on."""
if not self.available:
return False
return bool(self._hm_get_state())
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
if not self.available:
return None
# If state is MOTION (RemoteMotion works only)
if self._state == "MOTION":
return "motion"
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None)
def _check_hm_to_ha_object(self):
"""Check if possible to use the HM Object as this HA type."""
from pyhomematic.devicetypes.sensors import HMBinarySensor\
as pyHMBinarySensor
# Check compatibility from HMDevice
if not super()._check_hm_to_ha_object():
return False
# check if the Homematic device correct for this HA device
if not isinstance(self._hmdevice, pyHMBinarySensor):
_LOGGER.critical("This %s can't be use as binary", self._name)
return False
# if exists user value?
if self._state and self._state not in self._hmdevice.BINARYNODE:
_LOGGER.critical("This %s have no binary with %s", self._name,
self._state)
return False
# only check and give a warning to the user
if self._state is None and len(self._hmdevice.BINARYNODE) > 1:
_LOGGER.critical("%s have multiple binary params. It use all "
"binary nodes as one. Possible param values: %s",
self._name, str(self._hmdevice.BINARYNODE))
return False
return True
def _init_data_struct(self):
"""Generate a data struct (self._data) from the Homematic metadata."""
super()._init_data_struct()
# object have 1 binary
if self._state is None and len(self._hmdevice.BINARYNODE) == 1:
for value in self._hmdevice.BINARYNODE:
self._state = value
# add state to data struct
if self._state:
_LOGGER.debug("%s init datastruct with main node '%s'", self._name,
self._state)
self._data.update({self._state: STATE_UNKNOWN})
@@ -0,0 +1,24 @@
"""
Contains functionality to use a KNX group address as a binary.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.knx/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.knx import (
KNXConfig, KNXGroupAddress)
DEPENDENCIES = ["knx"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Setup the KNX binary sensor platform."""
add_entities([
KNXSwitch(hass, KNXConfig(config))
])
class KNXSwitch(KNXGroupAddress, BinarySensorDevice):
"""Representation of a KNX binary sensor device."""
pass
@@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup nx584 sensors."""
"""Setup nx584 binary sensor platform."""
from nx584 import client as nx584_client
host = config.get('host', 'localhost:5007')
+21 -39
View File
@@ -5,12 +5,15 @@ For more details about this platform, please refer to the documentation at
at https://home-assistant.io/components/sensor.wink/
"""
import logging
import json
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.components.sensor.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.7']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
@@ -22,7 +25,7 @@ SENSOR_TYPES = {
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink platform."""
"""Setup the Wink binary sensor platform."""
import pywink
if discovery_info is None:
@@ -40,17 +43,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if sensor.capability() in SENSOR_TYPES:
add_devices([WinkBinarySensorDevice(sensor)])
for key in pywink.get_keys():
add_devices([WinkBinarySensorDevice(key)])
class WinkBinarySensorDevice(BinarySensorDevice, Entity):
"""Representation of a Wink sensor."""
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink binary sensor."""
def __init__(self, wink):
"""Initialize the Wink binary sensor."""
self.wink = wink
super().__init__(wink)
wink = get_component('wink')
self._unit_of_measurement = self.wink.UNIT
self._battery = self.wink.battery_level
self.capability = self.wink.capability()
def _pubnub_update(self, message, channel):
if 'data' in message:
json_data = json.dumps(message.get('data'))
else:
json_data = message
self.wink.pubnub_update(json.loads(json_data))
self.update_ha_state()
@property
def is_on(self):
"""Return true if the binary sensor is on."""
@@ -67,35 +81,3 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return SENSOR_TYPES.get(self.capability)
@property
def unique_id(self):
"""Return the ID of this wink sensor."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the sensor if any."""
return self.wink.name()
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def update(self):
"""Update state of the sensor."""
self.wink.update_state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100
@@ -12,7 +12,7 @@ DEPENDENCIES = ["zigbee"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create and add an entity based on the configuration."""
"""Setup the ZigBee binary sensor platform."""
add_entities([
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
])
@@ -8,6 +8,7 @@ import logging
import datetime
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time
from homeassistant.helpers.entity import Entity
from homeassistant.components import zwave
from homeassistant.components.binary_sensor import (
DOMAIN,
@@ -31,7 +32,7 @@ DEVICE_MAPPINGS = {
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Z-Wave platform for sensors."""
"""Setup the Z-Wave platform for binary sensors."""
if discovery_info is None or zwave.NETWORK is None:
return
@@ -61,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ZWaveBinarySensor(value, None)])
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
"""Representation of a binary sensor within Z-Wave."""
def __init__(self, value, sensor_class):
@@ -97,7 +98,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
self.update_ha_state()
class ZWaveTriggerSensor(ZWaveBinarySensor):
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
"""Representation of a stateless sensor within Z-Wave."""
def __init__(self, sensor_value, sensor_class, hass, re_arm_sec=60):
+2 -3
View File
@@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/
"""
import logging
import time
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@@ -81,8 +82,6 @@ class Camera(Entity):
def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from camera images."""
import eventlet
def stream():
"""Stream images as mjpeg stream."""
try:
@@ -99,7 +98,7 @@ class Camera(Entity):
last_image = img_bytes
eventlet.sleep(0.5)
time.sleep(0.5)
except GeneratorExit:
pass
+1 -1
View File
@@ -18,7 +18,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoCamera(Camera):
"""A Demo camera."""
"""The representation of a Demo camera."""
def __init__(self, name):
"""Initialize demo camera component."""
+1 -1
View File
@@ -89,7 +89,7 @@ class WelcomeData(object):
"""Return all module available on the API as a list."""
self.update()
if not self.home:
for home in self.welcomedata.cameras.keys():
for home in self.welcomedata.cameras:
for camera in self.welcomedata.cameras[home].values():
self.camera_names.append(camera['name'])
else:
@@ -1,5 +1,9 @@
"""Camera platform that has a Raspberry Pi camera."""
"""
Camera platform that has a Raspberry Pi camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.rpi_camera/
"""
import os
import subprocess
import logging
@@ -43,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class RaspberryCamera(Camera):
"""Raspberry Pi camera."""
"""Representation of a Raspberry Pi camera."""
def __init__(self, device_info):
"""Initialize Raspberry Pi camera component."""
+3 -3
View File
@@ -27,7 +27,7 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REQUIREMENTS = ['fuzzywuzzy==0.10.0']
REQUIREMENTS = ['fuzzywuzzy==0.11.0']
def setup(hass, config):
@@ -67,8 +67,8 @@ def setup(hass, config):
}, blocking=True)
else:
logger.error(
'Got unsupported command %s from text %s', command, text)
logger.error('Got unsupported command %s from text %s',
command, text)
hass.services.register(DOMAIN, SERVICE_PROCESS, process,
schema=SERVICE_PROCESS_SCHEMA)
+6
View File
@@ -37,6 +37,7 @@ def setup(hass, config):
"""Setup a demo environment."""
group = loader.get_component('group')
configurator = loader.get_component('configurator')
persistent_notification = loader.get_component('persistent_notification')
config.setdefault(ha.DOMAIN, {})
config.setdefault(DOMAIN, {})
@@ -59,6 +60,11 @@ def setup(hass, config):
demo_config[component] = {CONF_PLATFORM: 'demo'}
bootstrap.setup_component(hass, component, demo_config)
# Setup example persistent notification
persistent_notification.create(
hass, 'This is an example of a persistent notification.',
title='Example Notification')
# Setup room groups
lights = sorted(hass.states.entity_ids('light'))
switches = sorted(hass.states.entity_ids('switch'))
@@ -377,12 +377,16 @@ def load_config(path, hass, consider_home, home_range):
"""Load devices from YAML configuration file."""
if not os.path.isfile(path):
return []
return [
Device(hass, consider_home, home_range, device.get('track', False),
str(dev_id).lower(), str(device.get('mac')).upper(),
device.get('name'), device.get('picture'),
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
for dev_id, device in load_yaml_config_file(path).items()]
try:
return [
Device(hass, consider_home, home_range, device.get('track', False),
str(dev_id).lower(), str(device.get('mac')).upper(),
device.get('name'), device.get('picture'),
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
for dev_id, device in load_yaml_config_file(path).items()]
except HomeAssistantError:
# When YAML file could not be loaded/did not contain a dict
return []
def setup_scanner_platform(hass, config, scanner, see_device):
@@ -62,8 +62,9 @@ def get_scanner(hass, config):
_LOGGER):
return None
elif CONF_PASSWORD not in config[DOMAIN] and \
'ssh_key' not in config[DOMAIN] and \
'pub_key' not in config[DOMAIN]:
_LOGGER.error("Either a public key or password must be provided")
_LOGGER.error('Either a private key or password must be provided')
return None
scanner = AsusWrtDeviceScanner(config[DOMAIN])
@@ -83,8 +84,8 @@ class AsusWrtDeviceScanner(object):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME])
self.password = str(config.get(CONF_PASSWORD))
self.pub_key = str(config.get('pub_key'))
self.password = str(config.get(CONF_PASSWORD, ''))
self.ssh_key = str(config.get('ssh_key', config.get('pub_key', '')))
self.protocol = config.get('protocol')
self.mode = config.get('mode')
@@ -120,7 +121,7 @@ class AsusWrtDeviceScanner(object):
return False
with self.lock:
_LOGGER.info("Checking ARP")
_LOGGER.info('Checking ARP')
data = self.get_asuswrt_data()
if not data:
return False
@@ -138,12 +139,12 @@ class AsusWrtDeviceScanner(object):
try:
ssh = pxssh.pxssh()
if self.pub_key:
ssh.login(self.host, self.username, ssh_key=self.pub_key)
if self.ssh_key:
ssh.login(self.host, self.username, ssh_key=self.ssh_key)
elif self.password:
ssh.login(self.host, self.username, self.password)
else:
_LOGGER.error('No password or public key specified')
_LOGGER.error('No password or private key specified')
return None
ssh.sendline(_IP_NEIGH_CMD)
ssh.prompt()
@@ -195,16 +196,16 @@ class AsusWrtDeviceScanner(object):
telnet.write('exit\n'.encode('ascii'))
return AsusWrtResult(neighbors, leases_result, arp_result)
except EOFError:
_LOGGER.error("Unexpected response from router")
_LOGGER.error('Unexpected response from router')
return None
except ConnectionRefusedError:
_LOGGER.error("Connection refused by router, is telnet enabled?")
_LOGGER.error('Connection refused by router, is telnet enabled?')
return None
except socket.gaierror as exc:
_LOGGER.error("Socket exception: %s", exc)
_LOGGER.error('Socket exception: %s', exc)
return None
except OSError as exc:
_LOGGER.error("OSError: %s", exc)
_LOGGER.error('OSError: %s', exc)
return None
def get_asuswrt_data(self):
@@ -232,7 +233,7 @@ class AsusWrtDeviceScanner(object):
match = _WL_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse wl row: %s", lease)
_LOGGER.warning('Could not parse wl row: %s', lease)
continue
host = ''
@@ -242,7 +243,7 @@ class AsusWrtDeviceScanner(object):
if match.group('mac').lower() in arp.decode('utf-8'):
arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
if not arp_match:
_LOGGER.warning("Could not parse arp row: %s", arp)
_LOGGER.warning('Could not parse arp row: %s', arp)
continue
devices[arp_match.group('ip')] = {
@@ -256,7 +257,7 @@ class AsusWrtDeviceScanner(object):
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
_LOGGER.warning('Could not parse lease row: %s', lease)
continue
# For leases where the client doesn't set a hostname, ensure it
@@ -275,7 +276,7 @@ class AsusWrtDeviceScanner(object):
for neighbor in result.neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
_LOGGER.warning('Could not parse neighbor row: %s', neighbor)
continue
if match.group('ip') in devices:
devices[match.group('ip')]['status'] = match.group('status')
@@ -16,6 +16,7 @@ REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
_LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'
CONF_SITE_ID = 'site_id'
def get_scanner(hass, config):
@@ -32,6 +33,7 @@ def get_scanner(hass, config):
host = this_config.get(CONF_HOST, 'localhost')
username = this_config.get(CONF_USERNAME)
password = this_config.get(CONF_PASSWORD)
site_id = this_config.get(CONF_SITE_ID, 'default')
try:
port = int(this_config.get(CONF_PORT, 8443))
@@ -40,7 +42,7 @@ def get_scanner(hass, config):
return False
try:
ctrl = Controller(host, username, password, port, 'v4')
ctrl = Controller(host, username, password, port, 'v4', site_id)
except urllib.error.HTTPError as ex:
_LOGGER.error('Failed to connect to unifi: %s', ex)
return False
+210
View File
@@ -0,0 +1,210 @@
"""
Support for Envisalink devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/envisalink/
"""
import logging
import time
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
from homeassistant.components.discovery import load_platform
REQUIREMENTS = ['pyenvisalink==1.0', 'pydispatcher==2.0.5']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'envisalink'
EVL_CONTROLLER = None
CONF_EVL_HOST = 'host'
CONF_EVL_PORT = 'port'
CONF_PANEL_TYPE = 'panel_type'
CONF_EVL_VERSION = 'evl_version'
CONF_CODE = 'code'
CONF_USERNAME = 'user_name'
CONF_PASS = 'password'
CONF_EVL_KEEPALIVE = 'keepalive_interval'
CONF_ZONEDUMP_INTERVAL = 'zonedump_interval'
CONF_ZONES = 'zones'
CONF_PARTITIONS = 'partitions'
CONF_ZONENAME = 'name'
CONF_ZONETYPE = 'type'
CONF_PARTITIONNAME = 'name'
DEFAULT_PORT = 4025
DEFAULT_EVL_VERSION = 3
DEFAULT_KEEPALIVE = 60
DEFAULT_ZONEDUMP_INTERVAL = 30
DEFAULT_ZONETYPE = 'opening'
SIGNAL_ZONE_UPDATE = 'zones_updated'
SIGNAL_PARTITION_UPDATE = 'partition_updated'
SIGNAL_KEYPAD_UPDATE = 'keypad_updated'
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONENAME): cv.string,
vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string})
PARTITION_SCHEMA = vol.Schema({
vol.Required(CONF_PARTITIONNAME): cv.string})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_EVL_HOST): cv.string,
vol.Required(CONF_PANEL_TYPE):
vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASS): cv.string,
vol.Required(CONF_CODE): cv.string,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION):
vol.All(vol.Coerce(int), vol.Range(min=3, max=4)),
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE):
vol.All(vol.Coerce(int), vol.Range(min=15)),
vol.Optional(CONF_ZONEDUMP_INTERVAL,
default=DEFAULT_ZONEDUMP_INTERVAL):
vol.All(vol.Coerce(int), vol.Range(min=15)),
}),
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument, too-many-function-args, too-many-locals
# pylint: disable=too-many-return-statements
def setup(hass, base_config):
"""Common setup for Envisalink devices."""
from pyenvisalink import EnvisalinkAlarmPanel
from pydispatch import dispatcher
global EVL_CONTROLLER
config = base_config.get(DOMAIN)
_host = config.get(CONF_EVL_HOST)
_port = config.get(CONF_EVL_PORT)
_code = config.get(CONF_CODE)
_panel_type = config.get(CONF_PANEL_TYPE)
_version = config.get(CONF_EVL_VERSION)
_user = config.get(CONF_USERNAME)
_pass = config.get(CONF_PASS)
_keep_alive = config.get(CONF_EVL_KEEPALIVE)
_zone_dump = config.get(CONF_ZONEDUMP_INTERVAL)
_zones = config.get(CONF_ZONES)
_partitions = config.get(CONF_PARTITIONS)
_connect_status = {}
EVL_CONTROLLER = EnvisalinkAlarmPanel(_host,
_port,
_panel_type,
_version,
_user,
_pass,
_zone_dump,
_keep_alive)
def login_fail_callback(data):
"""Callback for when the evl rejects our login."""
_LOGGER.error("The envisalink rejected your credentials.")
_connect_status['fail'] = 1
def connection_fail_callback(data):
"""Network failure callback."""
_LOGGER.error("Could not establish a connection with the envisalink.")
_connect_status['fail'] = 1
def connection_success_callback(data):
"""Callback for a successful connection."""
_LOGGER.info("Established a connection with the envisalink.")
_connect_status['success'] = 1
def zones_updated_callback(data):
"""Handle zone timer updates."""
_LOGGER.info("Envisalink sent a zone update event. Updating zones...")
dispatcher.send(signal=SIGNAL_ZONE_UPDATE,
sender=None,
zone=data)
def alarm_data_updated_callback(data):
"""Handle non-alarm based info updates."""
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE,
sender=None,
partition=data)
def partition_updated_callback(data):
"""Handle partition changes thrown by evl (including alarms)."""
_LOGGER.info("The envisalink sent a partition update event.")
dispatcher.send(signal=SIGNAL_PARTITION_UPDATE,
sender=None,
partition=data)
def stop_envisalink(event):
"""Shutdown envisalink connection and thread on exit."""
_LOGGER.info("Shutting down envisalink.")
EVL_CONTROLLER.stop()
def start_envisalink(event):
"""Startup process for the Envisalink."""
EVL_CONTROLLER.start()
for _ in range(10):
if 'success' in _connect_status:
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
return True
elif 'fail' in _connect_status:
return False
else:
time.sleep(1)
_LOGGER.error("Timeout occurred while establishing evl connection.")
return False
EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback
EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback
EVL_CONTROLLER.callback_partition_state_change = partition_updated_callback
EVL_CONTROLLER.callback_keypad_update = alarm_data_updated_callback
EVL_CONTROLLER.callback_login_failure = login_fail_callback
EVL_CONTROLLER.callback_login_timeout = connection_fail_callback
EVL_CONTROLLER.callback_login_success = connection_success_callback
_result = start_envisalink(None)
if not _result:
return False
# Load sub-components for Envisalink
if _partitions:
load_platform(hass, 'alarm_control_panel', 'envisalink',
{'partitions': _partitions,
'code': _code}, config)
load_platform(hass, 'sensor', 'envisalink',
{'partitions': _partitions,
'code': _code}, config)
if _zones:
load_platform(hass, 'binary_sensor', 'envisalink',
{'zones': _zones}, config)
return True
class EnvisalinkDevice(Entity):
"""Representation of an Envisalink device."""
def __init__(self, name, info, controller):
"""Initialize the device."""
self._controller = controller
self._info = info
self._name = name
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def should_poll(self):
"""No polling needed."""
return False
+14 -8
View File
@@ -1,9 +1,9 @@
"""Handle the frontend for Home Assistant."""
import os
from . import version, mdi_version
from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView
from . import version, mdi_version
DOMAIN = 'frontend'
DEPENDENCIES = ['api']
@@ -76,11 +76,17 @@ class IndexView(HomeAssistantView):
def get(self, request, entity_id=None):
"""Serve the index view."""
if self.hass.wsgi.development:
core_url = 'home-assistant-polymer/build/_core_compiled.js'
ui_url = 'home-assistant-polymer/src/home-assistant.html'
core_url = '/static/home-assistant-polymer/build/_core_compiled.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
map_url = ('/static/home-assistant-polymer/src/layouts/'
'partial-map.html')
dev_url = ('/static/home-assistant-polymer/src/entry-points/'
'dev-tools.html')
else:
core_url = 'core-{}.js'.format(version.CORE)
ui_url = 'frontend-{}.html'.format(version.UI)
core_url = '/static/core-{}.js'.format(version.CORE)
ui_url = '/static/frontend-{}.html'.format(version.UI)
map_url = '/static/partial-map-{}.html'.format(version.MAP)
dev_url = '/static/dev-tools-{}.html'.format(version.DEV)
# auto login if no password was set
if self.hass.config.api.api_password is None:
@@ -88,14 +94,14 @@ class IndexView(HomeAssistantView):
else:
auth = 'false'
icons_url = 'mdi-{}.html'.format(mdi_version.VERSION)
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
template = self.templates.get_template('index.html')
# pylint is wrong
# pylint: disable=no-member
resp = template.render(
core_url=core_url, ui_url=ui_url, auth=auth,
icons_url=icons_url, icons=mdi_version.VERSION)
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth,
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION)
return self.Response(resp, mimetype='text/html')
@@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "9ee3d4466a65bef35c2c8974e91b37c0"
VERSION = "758957b7ea989d6beca60e218ea7f7dd"
@@ -64,8 +64,12 @@
document
.getElementById('ha-init-skeleton')
.classList.add('error');
}
window.noAuth = {{ auth }}
};
window.noAuth = {{ auth }};
window.deferredLoading = {
map: '{{ map_url }}',
dev: '{{ dev_url }}',
};
</script>
</head>
<body fullbleed>
@@ -76,9 +80,9 @@
</div>
<home-assistant icons='{{ icons }}'></home-assistant>
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<script src='/static/{{ core_url }}'></script>
<link rel='import' href='/static/{{ ui_url }}' onerror='initError()' async>
<link rel='import' href='/static/{{ icons_url }}' async>
<script src='{{ core_url }}'></script>
<link rel='import' href='{{ ui_url }}' onerror='initError()' async>
<link rel='import' href='{{ icons_url }}' async>
<script>
var webComponentsSupported = (
'registerElement' in document &&
+4 -2
View File
@@ -1,3 +1,5 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
CORE = "7962327e4a29e51d4a6f4ee6cca9acc3"
UI = "570e1b8744a58024fc4e256f5e024424"
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
UI = "805f8dda70419b26daabc8e8f625127f"
MAP = "c922306de24140afd14f857f927bf8f0"
DEV = "b7079ac3121b95b9856e5603a6d8a263"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -29,7 +29,7 @@
/* eslint-disable quotes, comma-spacing */
var PrecacheConfig = [["/","595e12c9755af231fd80191e4cc74d2e"],["/devEvent","595e12c9755af231fd80191e4cc74d2e"],["/devInfo","595e12c9755af231fd80191e4cc74d2e"],["/devService","595e12c9755af231fd80191e4cc74d2e"],["/devState","595e12c9755af231fd80191e4cc74d2e"],["/devTemplate","595e12c9755af231fd80191e4cc74d2e"],["/history","595e12c9755af231fd80191e4cc74d2e"],["/logbook","595e12c9755af231fd80191e4cc74d2e"],["/map","595e12c9755af231fd80191e4cc74d2e"],["/states","595e12c9755af231fd80191e4cc74d2e"],["/static/core-7962327e4a29e51d4a6f4ee6cca9acc3.js","9c07ffb3f81cfb74f8a051b80cc8f9f0"],["/static/frontend-570e1b8744a58024fc4e256f5e024424.html","595e12c9755af231fd80191e4cc74d2e"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
var PrecacheConfig = [["/","d2c67846acf9a583c29798c30503cbf1"],["/devEvent","c4cdd84093404ee3fe0896070ebde97f"],["/devInfo","c4cdd84093404ee3fe0896070ebde97f"],["/devService","c4cdd84093404ee3fe0896070ebde97f"],["/devState","c4cdd84093404ee3fe0896070ebde97f"],["/devTemplate","c4cdd84093404ee3fe0896070ebde97f"],["/history","d2c67846acf9a583c29798c30503cbf1"],["/logbook","d2c67846acf9a583c29798c30503cbf1"],["/map","df0c87260b6dd990477cda43a2440b1c"],["/states","d2c67846acf9a583c29798c30503cbf1"],["/static/core-7d80cc0e4dea6bc20fa2889be0b3cd15.js","1f35577e9f32a86a03944e5e8d15eab2"],["/static/dev-tools-b7079ac3121b95b9856e5603a6d8a263.html","4ba7c57b48c9d28a1e0d9d7624b83700"],["/static/frontend-805f8dda70419b26daabc8e8f625127f.html","d8eeb403baf5893de8404beec0135d96"],["/static/mdi-758957b7ea989d6beca60e218ea7f7dd.html","4c32b01a3a5b194630963ff7ec4df36f"],["/static/partial-map-c922306de24140afd14f857f927bf8f0.html","853772ea26ac2f4db0f123e20c1ca160"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
/* eslint-enable quotes, comma-spacing */
var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-';
@@ -0,0 +1,96 @@
"""
Support for building a Raspberry Pi garage controller in HA.
Instructions for building the controller can be found here
https://github.com/andrewshilliday/garage-door-controller
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/garage_door.rpi_gpio/
"""
import logging
from time import sleep
import voluptuous as vol
from homeassistant.components.garage_door import GarageDoorDevice
import homeassistant.components.rpi_gpio as rpi_gpio
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['rpi_gpio']
_LOGGER = logging.getLogger(__name__)
_DOORS_SCHEMA = vol.All(
cv.ensure_list,
[
vol.Schema({
'name': str,
'relay_pin': int,
'state_pin': int,
})
]
)
PLATFORM_SCHEMA = vol.Schema({
'platform': str,
vol.Required('doors'): _DOORS_SCHEMA,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the garage door platform."""
doors = []
doors_conf = config.get('doors')
for door in doors_conf:
doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'],
door['state_pin']))
add_devices(doors)
class RPiGPIOGarageDoor(GarageDoorDevice):
"""Representation of a Raspberry garage door."""
def __init__(self, name, relay_pin, state_pin):
"""Initialize the garage door."""
self._name = name
self._state = False
self._relay_pin = relay_pin
self._state_pin = state_pin
rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, 'UP')
rpi_gpio.write_output(self._relay_pin, True)
@property
def unique_id(self):
"""Return the ID of this garage door."""
return "{}.{}".format(self.__class__, self._name)
@property
def name(self):
"""Return the name of the garage door if any."""
return self._name
def update(self):
"""Update the state of the garage door."""
self._state = rpi_gpio.read_input(self._state_pin) is True
@property
def is_closed(self):
"""Return true if door is closed."""
return self._state
def _trigger(self):
"""Trigger the door."""
rpi_gpio.write_output(self._relay_pin, False)
sleep(0.2)
rpi_gpio.write_output(self._relay_pin, True)
def close_door(self):
"""Close the door."""
if not self.is_closed:
self._trigger()
def open_door(self):
"""Open the door."""
if self.is_closed:
self._trigger()
+5 -37
View File
@@ -7,9 +7,10 @@ https://home-assistant.io/components/garage_door.wink/
import logging
from homeassistant.components.garage_door import GarageDoorDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.7']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pywink.get_garage_doors())
class WinkGarageDoorDevice(GarageDoorDevice):
class WinkGarageDoorDevice(WinkDevice, GarageDoorDevice):
"""Representation of a Wink garage door."""
def __init__(self, wink):
"""Initialize the garage door."""
self.wink = wink
self._battery = self.wink.battery_level
@property
def unique_id(self):
"""Return the ID of this wink garage door."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the garage door if any."""
return self.wink.name()
def update(self):
"""Update the state of the garage door."""
self.wink.update_state()
WinkDevice.__init__(self, wink)
@property
def is_closed(self):
"""Return true if door is closed."""
return self.wink.state() == 0
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def close_door(self):
"""Close the door."""
self.wink.set_state(0)
@@ -70,16 +51,3 @@ class WinkGarageDoorDevice(GarageDoorDevice):
def open_door(self):
"""Open the door."""
self.wink.set_state(1)
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100
@@ -0,0 +1,70 @@
"""
Support for Zwave garage door components.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/garagedoor.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.garage_door import DOMAIN
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.garage_door import GarageDoorDevice
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Z-Wave garage door device."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY:
return
if value.type != zwave.TYPE_BOOL:
return
if value.genre != zwave.GENRE_USER:
return
value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)])
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
"""Representation of an Zwave garage door device."""
def __init__(self, value):
"""Initialize the zwave garage door."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
self._state = value.data
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
self._state = value.data
self.update_ha_state(True)
_LOGGER.debug("Value changed on network %s", value)
@property
def is_closed(self):
"""Return the current position of Zwave garage door."""
return not self._state
def close_door(self):
"""Close the garage door."""
self._value.node.set_switch(self._value.value_id, False)
def open_door(self):
"""Open the garage door."""
self._value.node.set_switch(self._value.value_id, True)
+122
View File
@@ -0,0 +1,122 @@
"""
CEC component.
Requires libcec + Python bindings.
"""
import logging
import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
_CEC = None
DOMAIN = 'hdmi_cec'
SERVICE_SELECT_DEVICE = 'select_device'
SERVICE_POWER_ON = 'power_on'
SERVICE_STANDBY = 'standby'
CONF_DEVICES = 'devices'
ATTR_DEVICE = 'device'
MAX_DEPTH = 4
# pylint: disable=unnecessary-lambda
DEVICE_SCHEMA = vol.Schema({
vol.All(cv.positive_int): vol.Any(lambda devices: DEVICE_SCHEMA(devices),
cv.string)
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICES): DEVICE_SCHEMA
})
}, extra=vol.ALLOW_EXTRA)
def parse_mapping(mapping, parents=None):
"""Parse configuration device mapping."""
if parents is None:
parents = []
for addr, val in mapping.items():
cur = parents + [str(addr)]
if isinstance(val, dict):
yield from parse_mapping(val, cur)
elif isinstance(val, str):
yield (val, cur)
def pad_physical_address(addr):
"""Right-pad a physical address."""
return addr + ['0'] * (MAX_DEPTH - len(addr))
def setup(hass, config):
"""Setup CEC capability."""
global _CEC
# cec is only available if libcec is properly installed
# and the Python bindings are accessible.
try:
import cec
except ImportError:
_LOGGER.error("libcec must be installed")
return False
# Parse configuration into a dict of device name
# to physical address represented as a list of
# four elements.
flat = {}
for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})):
flat[pair[0]] = pad_physical_address(pair[1])
# Configure libcec.
cfg = cec.libcec_configuration()
cfg.strDeviceName = 'HASS'
cfg.bActivateSource = 0
cfg.bMonitorOnly = 1
cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT
# Set up CEC adapter.
_CEC = cec.ICECAdapter.Create(cfg)
def _power_on(call):
"""Power on all devices."""
_CEC.PowerOnDevices()
def _standby(call):
"""Standby all devices."""
_CEC.StandbyDevices()
def _select_device(call):
"""Select the active device."""
path = flat.get(call.data[ATTR_DEVICE])
if not path:
_LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE])
cmds = []
for i in range(1, MAX_DEPTH - 1):
addr = pad_physical_address(path[:i])
cmds.append('1f:82:{}{}:{}{}'.format(*addr))
cmds.append('1f:86:{}{}:{}{}'.format(*addr))
for cmd in cmds:
_CEC.Transmit(_CEC.CommandFromString(cmd))
_LOGGER.info("Selected %s", call.data[ATTR_DEVICE])
def _start_cec(event):
"""Open CEC adapter."""
adapters = _CEC.DetectAdapters()
if len(adapters) == 0:
_LOGGER.error("No CEC adapter found")
return
if _CEC.Open(adapters[0].strComName):
hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on)
hass.services.register(DOMAIN, SERVICE_STANDBY, _standby)
hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE,
_select_device)
else:
_LOGGER.error("Failed to open adapter")
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec)
return True
+44 -48
View File
@@ -9,8 +9,8 @@ from collections import defaultdict
from datetime import timedelta
from itertools import groupby
from homeassistant.components import recorder, script
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, script
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'history'
@@ -27,13 +27,12 @@ def last_5_states(entity_id):
"""Return the last 5 states for entity_id."""
entity_id = entity_id.lower()
query = """
SELECT * FROM states WHERE entity_id=? AND
last_changed=last_updated
ORDER BY state_id DESC LIMIT 0, 5
"""
return recorder.query_states(query, (entity_id, ))
states = recorder.get_model('States')
return recorder.execute(
recorder.query('States').filter(
(states.entity_id == entity_id) &
(states.last_changed == states.last_updated)
).order_by(states.state_id.desc()).limit(5))
def get_significant_states(start_time, end_time=None, entity_id=None):
@@ -44,48 +43,42 @@ def get_significant_states(start_time, end_time=None, entity_id=None):
as well as all states from certain domains (for instance
thermostat so that we get current temperature in our graphs).
"""
where = """
(domain IN ({}) OR last_changed=last_updated)
AND domain NOT IN ({}) AND last_updated > ?
""".format(",".join("'%s'" % x for x in SIGNIFICANT_DOMAINS),
",".join("'%s'" % x for x in IGNORE_DOMAINS))
data = [start_time]
states = recorder.get_model('States')
query = recorder.query('States').filter(
(states.domain.in_(SIGNIFICANT_DOMAINS) |
(states.last_changed == states.last_updated)) &
((~states.domain.in_(IGNORE_DOMAINS)) &
(states.last_updated > start_time)))
if end_time is not None:
where += "AND last_updated < ? "
data.append(end_time)
query = query.filter(states.last_updated < end_time)
if entity_id is not None:
where += "AND entity_id = ? "
data.append(entity_id.lower())
query = query.filter_by(entity_id=entity_id.lower())
query = ("SELECT * FROM states WHERE {} "
"ORDER BY entity_id, last_updated ASC").format(where)
states = (state for state in recorder.query_states(query, data)
if _is_significant(state))
states = (
state for state in recorder.execute(
query.order_by(states.entity_id, states.last_updated))
if _is_significant(state))
return states_to_json(states, start_time, entity_id)
def state_changes_during_period(start_time, end_time=None, entity_id=None):
"""Return states changes during UTC period start_time - end_time."""
where = "last_changed=last_updated AND last_changed > ? "
data = [start_time]
states = recorder.get_model('States')
query = recorder.query('States').filter(
(states.last_changed == states.last_updated) &
(states.last_changed > start_time))
if end_time is not None:
where += "AND last_changed < ? "
data.append(end_time)
query = query.filter(states.last_updated < end_time)
if entity_id is not None:
where += "AND entity_id = ? "
data.append(entity_id.lower())
query = query.filter_by(entity_id=entity_id.lower())
query = ("SELECT * FROM states WHERE {} "
"ORDER BY entity_id, last_changed ASC").format(where)
states = recorder.query_states(query, data)
states = recorder.execute(
query.order_by(states.entity_id, states.last_updated))
return states_to_json(states, start_time, entity_id)
@@ -99,24 +92,27 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
if run is None:
return []
where = run.where_after_start_run + "AND created < ? "
where_data = [utc_point_in_time]
from sqlalchemy import and_, func
states = recorder.get_model('States')
most_recent_state_ids = recorder.query(
func.max(states.state_id).label('max_state_id')
).filter(
(states.created >= run.start) &
(states.created < utc_point_in_time)
)
if entity_ids is not None:
where += "AND entity_id IN ({}) ".format(
",".join(['?'] * len(entity_ids)))
where_data.extend(entity_ids)
most_recent_state_ids = most_recent_state_ids.filter(
states.entity_id.in_(entity_ids))
query = """
SELECT * FROM states
INNER JOIN (
SELECT max(state_id) AS max_state_id
FROM states WHERE {}
GROUP BY entity_id)
WHERE state_id = max_state_id
""".format(where)
most_recent_state_ids = most_recent_state_ids.group_by(
states.entity_id).subquery()
return recorder.query_states(query, where_data)
query = recorder.query('States').join(most_recent_state_ids, and_(
states.state_id == most_recent_state_ids.c.max_state_id))
return recorder.execute(query)
def states_to_json(states, start_time, entity_id):
+625
View File
@@ -0,0 +1,625 @@
"""
Support for Homematic devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematic/
"""
import os
import time
import logging
from functools import partial
import voluptuous as vol
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN,
CONF_USERNAME, CONF_PASSWORD)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import discovery
from homeassistant.config import load_yaml_config_file
DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.9"]
HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5
DISCOVER_SWITCHES = 'homematic.switch'
DISCOVER_LIGHTS = 'homematic.light'
DISCOVER_SENSORS = 'homematic.sensor'
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
DISCOVER_ROLLERSHUTTER = 'homematic.rollershutter'
DISCOVER_THERMOSTATS = 'homematic.thermostat'
ATTR_DISCOVER_DEVICES = 'devices'
ATTR_PARAM = 'param'
ATTR_CHANNEL = 'channel'
ATTR_NAME = 'name'
ATTR_ADDRESS = 'address'
EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse'
SERVICE_VIRTUALKEY = 'virtualkey'
HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'],
DISCOVER_LIGHTS: ['Dimmer'],
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2',
'RemoteMotion', 'ThermostatWall', 'AreaThermostat',
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas',
'LuxSensor'],
DISCOVER_THERMOSTATS: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion',
'MotionV2', 'RemoteMotion'],
DISCOVER_ROLLERSHUTTER: ['Blind']
}
HM_IGNORE_DISCOVERY_NODE = [
'ACTUAL_TEMPERATURE'
]
HM_ATTRIBUTE_SUPPORT = {
'LOWBAT': ['Battery', {0: 'High', 1: 'Low'}],
'ERROR': ['Sabotage', {0: 'No', 1: 'Yes'}],
'RSSI_DEVICE': ['RSSI', {}],
'VALVE_STATE': ['Valve', {}],
'BATTERY_STATE': ['Battery', {}],
'CONTROL_MODE': ['Mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}],
'POWER': ['Power', {}],
'CURRENT': ['Current', {}],
'VOLTAGE': ['Voltage', {}]
}
HM_PRESS_EVENTS = [
'PRESS_SHORT',
'PRESS_LONG',
'PRESS_CONT',
'PRESS_LONG_RELEASE'
]
HM_IMPULSE_EVENTS = [
'SEQUENCE_OK'
]
_LOGGER = logging.getLogger(__name__)
CONF_RESOLVENAMES_OPTIONS = [
'metadata',
'json',
'xml',
False
]
CONF_LOCAL_IP = 'local_ip'
CONF_LOCAL_PORT = 'local_port'
CONF_REMOTE_IP = 'remote_ip'
CONF_REMOTE_PORT = 'remote_port'
CONF_RESOLVENAMES = 'resolvenames'
CONF_DELAY = 'delay'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_LOCAL_IP): vol.Coerce(str),
vol.Optional(CONF_LOCAL_PORT, default=8943):
vol.All(vol.Coerce(int),
vol.Range(min=1, max=65535)),
vol.Required(CONF_REMOTE_IP): vol.Coerce(str),
vol.Optional(CONF_REMOTE_PORT, default=2001):
vol.All(vol.Coerce(int),
vol.Range(min=1, max=65535)),
vol.Optional(CONF_RESOLVENAMES, default=False):
vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_USERNAME, default="Admin"): vol.Coerce(str),
vol.Optional(CONF_PASSWORD, default=""): vol.Coerce(str),
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float)
})
SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.Coerce(str),
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): vol.Coerce(str)
})
# pylint: disable=unused-argument
def setup(hass, config):
"""Setup the Homematic component."""
global HOMEMATIC, HOMEMATIC_LINK_DELAY
from pyhomematic import HMConnection
local_ip = config[DOMAIN][0].get(CONF_LOCAL_IP)
local_port = config[DOMAIN][0].get(CONF_LOCAL_PORT)
remote_ip = config[DOMAIN][0].get(CONF_REMOTE_IP)
remote_port = config[DOMAIN][0].get(CONF_REMOTE_PORT)
resolvenames = config[DOMAIN][0].get(CONF_RESOLVENAMES)
username = config[DOMAIN][0].get(CONF_USERNAME)
password = config[DOMAIN][0].get(CONF_PASSWORD)
HOMEMATIC_LINK_DELAY = config[DOMAIN][0].get(CONF_DELAY)
if remote_ip is None or local_ip is None:
_LOGGER.error("Missing remote CCU/Homegear or local address")
return False
# Create server thread
bound_system_callback = partial(system_callback_handler, hass, config)
HOMEMATIC = HMConnection(local=local_ip,
localport=local_port,
remote=remote_ip,
remoteport=remote_port,
systemcallback=bound_system_callback,
resolvenames=resolvenames,
rpcusername=username,
rpcpassword=password,
interface_id="homeassistant")
# Start server thread, connect to peer, initialize to receive events
HOMEMATIC.start()
# Stops server when Homeassistant is shutting down
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop)
hass.config.components.append(DOMAIN)
# regeister homematic services
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
_hm_service_virtualkey,
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
SCHEMA_SERVICE_VIRTUALKEY)
return True
# pylint: disable=too-many-branches
def system_callback_handler(hass, config, src, *args):
"""Callback handler."""
if src == 'newDevices':
_LOGGER.debug("newDevices with: %s", str(args))
# pylint: disable=unused-variable
(interface_id, dev_descriptions) = args
key_dict = {}
# Get list of all keys of the devices (ignoring channels)
for dev in dev_descriptions:
key_dict[dev['ADDRESS'].split(':')[0]] = True
# Register EVENTS
# Search all device with a EVENTNODE that include data
bound_event_callback = partial(_hm_event_handler, hass)
for dev in key_dict:
if dev not in HOMEMATIC.devices:
continue
hmdevice = HOMEMATIC.devices.get(dev)
# have events?
if len(hmdevice.EVENTNODE) > 0:
_LOGGER.debug("Register Events from %s", dev)
hmdevice.setEventCallback(callback=bound_event_callback,
bequeath=True)
# If configuration allows autodetection of devices,
# all devices not configured are added.
if key_dict:
for component_name, discovery_type in (
('switch', DISCOVER_SWITCHES),
('light', DISCOVER_LIGHTS),
('rollershutter', DISCOVER_ROLLERSHUTTER),
('binary_sensor', DISCOVER_BINARY_SENSORS),
('sensor', DISCOVER_SENSORS),
('thermostat', DISCOVER_THERMOSTATS)):
# Get all devices of a specific type
found_devices = _get_devices(discovery_type, key_dict)
# When devices of this type are found
# they are setup in HA and an event is fired
if found_devices:
# Fire discovery event
discovery.load_platform(hass, component_name, DOMAIN, {
ATTR_DISCOVER_DEVICES: found_devices
}, config)
def _get_devices(device_type, keys):
"""Get the Homematic devices."""
# run
device_arr = []
for key in keys:
device = HOMEMATIC.devices[key]
class_name = device.__class__.__name__
metadata = {}
# is class supported by discovery type
if class_name not in HM_DEVICE_TYPES[device_type]:
continue
# Load metadata if needed to generate a param list
if device_type == DISCOVER_SENSORS:
metadata.update(device.SENSORNODE)
elif device_type == DISCOVER_BINARY_SENSORS:
metadata.update(device.BINARYNODE)
params = _create_params_list(device, metadata, device_type)
if params:
# Generate options for 1...n elements with 1...n params
for channel in range(1, device.ELEMENT + 1):
_LOGGER.debug("Handling %s:%i", key, channel)
if channel in params:
for param in params[channel]:
name = _create_ha_name(name=device.NAME,
channel=channel,
param=param)
device_dict = dict(platform="homematic",
address=key,
name=name,
channel=channel)
if param is not None:
device_dict[ATTR_PARAM] = param
# Add new device
device_arr.append(device_dict)
else:
_LOGGER.debug("Channel %i not in params", channel)
else:
_LOGGER.debug("Got no params for %s", key)
_LOGGER.debug("%s autodiscovery: %s",
device_type, str(device_arr))
return device_arr
def _create_params_list(hmdevice, metadata, device_type):
"""Create a list from HMDevice with all possible parameters in config."""
params = {}
merge = False
# use merge?
if device_type == DISCOVER_SENSORS:
merge = True
elif device_type == DISCOVER_BINARY_SENSORS:
merge = True
# Search in sensor and binary metadata per elements
for channel in range(1, hmdevice.ELEMENT + 1):
param_chan = []
for node, meta_chan in metadata.items():
try:
# Is this attribute ignored?
if node in HM_IGNORE_DISCOVERY_NODE:
continue
if meta_chan == 'c' or meta_chan is None:
# Only channel linked data
param_chan.append(node)
elif channel == 1:
# First channel can have other data channel
param_chan.append(node)
except (TypeError, ValueError):
_LOGGER.error("Exception generating %s (%s)",
hmdevice.ADDRESS, str(metadata))
# default parameter is merge is off
if len(param_chan) == 0 and not merge:
param_chan.append(None)
# Add to channel
if len(param_chan) > 0:
params.update({channel: param_chan})
_LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS,
str(params))
return params
def _create_ha_name(name, channel, param):
"""Generate a unique object name."""
# HMDevice is a simple device
if channel == 1 and param is None:
return name
# Has multiple elements/channels
if channel > 1 and param is None:
return "{} {}".format(name, channel)
# With multiple param first elements
if channel == 1 and param is not None:
return "{} {}".format(name, param)
# Multiple param on object with multiple elements
if channel > 1 and param is not None:
return "{} {} {}".format(name, channel, param)
def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info,
add_callback_devices):
"""Helper to setup Homematic devices with discovery info."""
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
_LOGGER.debug("Add device %s from config: %s",
str(hmdevicetype), str(config))
# create object and add to HA
new_device = hmdevicetype(config)
add_callback_devices([new_device])
# link to HM
new_device.link_homematic()
return True
def _hm_event_handler(hass, device, caller, attribute, value):
"""Handle all pyhomematic device events."""
try:
channel = int(device.split(":")[1])
address = device.split(":")[0]
hmdevice = HOMEMATIC.devices.get(address)
except (TypeError, ValueError):
_LOGGER.error("Event handling channel convert error!")
return
# is not a event?
if attribute not in hmdevice.EVENTNODE:
return
_LOGGER.debug("Event %s for %s channel %i", attribute,
hmdevice.NAME, channel)
# keypress event
if attribute in HM_PRESS_EVENTS:
hass.bus.fire(EVENT_KEYPRESS, {
ATTR_NAME: hmdevice.NAME,
ATTR_PARAM: attribute,
ATTR_CHANNEL: channel
})
return
# impulse event
if attribute in HM_IMPULSE_EVENTS:
hass.bus.fire(EVENT_KEYPRESS, {
ATTR_NAME: hmdevice.NAME,
ATTR_CHANNEL: channel
})
return
_LOGGER.warning("Event is unknown and not forwarded to HA")
def _hm_service_virtualkey(call):
"""Callback for handle virtualkey services."""
address = call.data.get(ATTR_ADDRESS)
channel = call.data.get(ATTR_CHANNEL)
param = call.data.get(ATTR_PARAM)
if address not in HOMEMATIC.devices:
_LOGGER.error("%s not found for service virtualkey!", address)
return
hmdevice = HOMEMATIC.devices.get(address)
# if param exists for this device
if param not in hmdevice.ACTIONNODE:
_LOGGER.error("%s not datapoint in hm device %s", param, address)
return
# channel exists?
if channel > hmdevice.ELEMENT:
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
return
# call key
hmdevice.actionNodeData(param, 1, channel)
class HMDevice(Entity):
"""The Homematic device base object."""
# pylint: disable=too-many-instance-attributes
def __init__(self, config):
"""Initialize a generic Homematic device."""
self._name = config.get(ATTR_NAME, None)
self._address = config.get(ATTR_ADDRESS, None)
self._channel = config.get(ATTR_CHANNEL, 1)
self._state = config.get(ATTR_PARAM, None)
self._data = {}
self._hmdevice = None
self._connected = False
self._available = False
# Set param to uppercase
if self._state:
self._state = self._state.upper()
# Generate name
if not self._name:
self._name = _create_ha_name(name=self._address,
channel=self._channel,
param=self._state)
@property
def should_poll(self):
"""Return false. Homematic states are pushed by the XML RPC Server."""
return False
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def assumed_state(self):
"""Return true if unable to access real state of the device."""
return not self._available
@property
def available(self):
"""Return true if device is available."""
return self._available
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
attr = {}
# no data available to create
if not self.available:
return attr
# Generate an attributes list
for node, data in HM_ATTRIBUTE_SUPPORT.items():
# Is an attributes and exists for this object
if node in self._data:
value = data[1].get(self._data[node], self._data[node])
attr[data[0]] = value
# static attributes
attr["ID"] = self._hmdevice.ADDRESS
return attr
def link_homematic(self):
"""Connect to Homematic."""
# device is already linked
if self._connected:
return True
# Does a HMDevice from pyhomematic exist?
if self._address in HOMEMATIC.devices:
# Init
self._hmdevice = HOMEMATIC.devices[self._address]
self._connected = True
# Check if Homematic class is okay for HA class
_LOGGER.info("Start linking %s to %s", self._address, self._name)
if self._check_hm_to_ha_object():
try:
# Init datapoints of this object
self._init_data_struct()
if HOMEMATIC_LINK_DELAY:
# We delay / pause loading of data to avoid overloading
# of CCU / Homegear when doing auto detection
time.sleep(HOMEMATIC_LINK_DELAY)
self._load_init_data_from_hm()
_LOGGER.debug("%s datastruct: %s",
self._name, str(self._data))
# Link events from pyhomatic
self._subscribe_homematic_events()
self._available = not self._hmdevice.UNREACH
# pylint: disable=broad-except
except Exception as err:
self._connected = False
_LOGGER.error("Exception while linking %s: %s",
self._address, str(err))
else:
_LOGGER.critical("Delink %s object from HM", self._name)
self._connected = False
# Update HA
_LOGGER.debug("%s linking done, send update_ha_state", self._name)
self.update_ha_state()
else:
_LOGGER.debug("%s not found in HOMEMATIC.devices", self._address)
def _hm_event_callback(self, device, caller, attribute, value):
"""Handle all pyhomematic device events."""
_LOGGER.debug("%s received event '%s' value: %s", self._name,
attribute, value)
have_change = False
# Is data needed for this instance?
if attribute in self._data:
# Did data change?
if self._data[attribute] != value:
self._data[attribute] = value
have_change = True
# If available it has changed
if attribute is "UNREACH":
self._available = bool(value)
have_change = True
# If it has changed data point, update HA
if have_change:
_LOGGER.debug("%s update_ha_state after '%s'", self._name,
attribute)
self.update_ha_state()
def _subscribe_homematic_events(self):
"""Subscribe all required events to handle job."""
channels_to_sub = {}
# Push data to channels_to_sub from hmdevice metadata
for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE,
self._hmdevice.ATTRIBUTENODE,
self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE,
self._hmdevice.ACTIONNODE):
for node, channel in metadata.items():
# Data is needed for this instance
if node in self._data:
# chan is current channel
if channel == 'c' or channel is None:
channel = self._channel
# Prepare for subscription
try:
if int(channel) >= 0:
channels_to_sub.update({int(channel): True})
except (ValueError, TypeError):
_LOGGER("Invalid channel in metadata from %s",
self._name)
# Set callbacks
for channel in channels_to_sub:
_LOGGER.debug("Subscribe channel %s from %s",
str(channel), self._name)
self._hmdevice.setEventCallback(callback=self._hm_event_callback,
bequeath=False,
channel=channel)
def _load_init_data_from_hm(self):
"""Load first value from pyhomematic."""
if not self._connected:
return False
# Read data from pyhomematic
for metadata, funct in (
(self._hmdevice.ATTRIBUTENODE,
self._hmdevice.getAttributeData),
(self._hmdevice.WRITENODE, self._hmdevice.getWriteData),
(self._hmdevice.SENSORNODE, self._hmdevice.getSensorData),
(self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)):
for node in metadata:
if node in self._data:
self._data[node] = funct(name=node, channel=self._channel)
return True
def _hm_set_state(self, value):
"""Set data to main datapoint."""
if self._state in self._data:
self._data[self._state] = value
def _hm_get_state(self):
"""Get data from main datapoint."""
if self._state in self._data:
return self._data[self._state]
return None
def _check_hm_to_ha_object(self):
"""Check if it is possible to use the Homematic object as this HA type.
NEEDS overwrite by inherit!
"""
if not self._connected or self._hmdevice is None:
_LOGGER.error("HA object is not linked to homematic.")
return False
# Check if button option is correctly set for this object
if self._channel > self._hmdevice.ELEMENT:
_LOGGER.critical("Button option is not correct for this object!")
return False
return True
def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata.
NEEDS overwrite by inherit!
"""
# Add all attributes to data dict
for data_note in self._hmdevice.ATTRIBUTENODE:
self._data.update({data_note: STATE_UNKNOWN})
+80 -17
View File
@@ -1,25 +1,31 @@
"""This module provides WSGI application to serve the Home Assistant API."""
"""
This module provides WSGI application to serve the Home Assistant API.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/http/
"""
import hmac
import json
import logging
import mimetypes
import threading
import re
import ssl
import voluptuous as vol
import homeassistant.core as ha
import homeassistant.remote as rem
from homeassistant import util
from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS)
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.helpers.entity import split_entity_id
import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv
DOMAIN = "http"
REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",)
REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10")
CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
@@ -31,6 +37,27 @@ CONF_CORS_ORIGINS = 'cors_allowed_origins'
DATA_API_PASSWORD = 'api_password'
# TLS configuation follows the best-practice guidelines
# specified here: https://wiki.mozilla.org/Security/Server_Side_TLS
# Intermediate guidelines are followed.
SSL_VERSION = ssl.PROTOCOL_SSLv23
SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
if hasattr(ssl, 'OP_NO_COMPRESSION'):
SSL_OPTS |= ssl.OP_NO_COMPRESSION
CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \
"DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \
"ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \
"ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \
"ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \
"DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \
"DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \
"ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \
"AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \
"AES256-SHA:DES-CBC3-SHA:!DSS"
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
_LOGGER = logging.getLogger(__name__)
@@ -93,11 +120,17 @@ def setup(hass, config):
cors_origins=cors_origins
)
hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START,
lambda event:
threading.Thread(target=server.start, daemon=True,
name='WSGI-server').start())
def start_wsgi_server(event):
"""Start the WSGI server."""
server.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server)
def stop_wsgi_server(event):
"""Stop the WSGI server."""
server.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server)
hass.wsgi = server
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
@@ -216,6 +249,7 @@ class HomeAssistantWSGI(object):
self.server_port = server_port
self.cors_origins = cors_origins
self.event_forwarder = None
self.server = None
def register_view(self, view):
"""Register a view with the WSGI server.
@@ -283,14 +317,34 @@ class HomeAssistantWSGI(object):
def start(self):
"""Start the wsgi server."""
from eventlet import wsgi
import eventlet
from cherrypy import wsgiserver
from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter
# pylint: disable=too-few-public-methods,super-init-not-called
class ContextSSLAdapter(BuiltinSSLAdapter):
"""SSL Adapter that takes in an SSL context."""
def __init__(self, context):
self.context = context
# pylint: disable=no-member
self.server = wsgiserver.CherryPyWSGIServer(
(self.server_host, self.server_port), self,
server_name='Home Assistant')
sock = eventlet.listen((self.server_host, self.server_port))
if self.ssl_certificate:
sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
keyfile=self.ssl_key, server_side=True)
wsgi.server(sock, self, log=_LOGGER)
context = ssl.SSLContext(SSL_VERSION)
context.options |= SSL_OPTS
context.set_ciphers(CIPHERS)
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
self.server.ssl_adapter = ContextSSLAdapter(context)
threading.Thread(target=self.server.start, daemon=True,
name='WSGI-server').start()
def stop(self):
"""Stop the wsgi server."""
self.server.stop()
def dispatch_request(self, request):
"""Handle incoming request."""
@@ -337,6 +391,10 @@ class HomeAssistantWSGI(object):
"""Handle a request for base app + extra apps."""
from werkzeug.wsgi import DispatcherMiddleware
if not self.hass.is_running:
from werkzeug.exceptions import BadRequest
return BadRequest()(environ, start_response)
app = DispatcherMiddleware(self.base_app, self.extra_apps)
# Strip out any cachebusting MD5 fingerprints
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
@@ -395,7 +453,12 @@ class HomeAssistantView(object):
self.hass.wsgi.api_password):
authenticated = True
if self.requires_auth and not authenticated:
if authenticated:
_LOGGER.info('Successful login/request from %s',
request.remote_addr)
elif self.requires_auth and not authenticated:
_LOGGER.warning('Login attempt or request with an invalid'
'password from %s', request.remote_addr)
raise Unauthorized()
request.authenticated = authenticated
@@ -437,7 +500,7 @@ class HomeAssistantView(object):
mimetype = mimetypes.guess_type(fil)[0]
try:
fil = open(fil)
fil = open(fil, mode='br')
except IOError:
raise NotFound()
+9 -9
View File
@@ -425,39 +425,39 @@ class HvacDevice(Entity):
def set_temperature(self, temperature):
"""Set new target temperature."""
pass
raise NotImplementedError()
def set_humidity(self, humidity):
"""Set new target humidity."""
pass
raise NotImplementedError()
def set_fan_mode(self, fan):
"""Set new target fan mode."""
pass
raise NotImplementedError()
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
pass
raise NotImplementedError()
def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
pass
raise NotImplementedError()
def turn_away_mode_on(self):
"""Turn away mode on."""
pass
raise NotImplementedError()
def turn_away_mode_off(self):
"""Turn away mode off."""
pass
raise NotImplementedError()
def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
pass
raise NotImplementedError()
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
pass
raise NotImplementedError()
@property
def min_temp(self):
+6 -2
View File
@@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments
# pylint: disable=too-many-arguments, abstract-method
class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
"""Represents a HeatControl hvac."""
@@ -98,7 +98,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.node == value.node:
if self._value.value_id == value.value_id:
self.update_properties()
self.update_ha_state(True)
_LOGGER.debug("Value changed on network %s", value)
@@ -211,6 +211,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
value.data = int(round(temperature, 0))
else:
value.data = int(temperature)
break
def set_fan_mode(self, fan):
"""Set new target fan mode."""
@@ -218,6 +219,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
if value.command_class == 68 and value.index == 0:
value.data = bytes(fan, 'utf-8')
break
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
@@ -225,6 +227,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
if value.command_class == 64 and value.index == 0:
value.data = bytes(operation_mode, 'utf-8')
break
def set_swing_mode(self, swing_mode):
"""Set new target swing mode."""
@@ -233,3 +236,4 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
value.data = int(swing_mode)
break
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_DATABASE = 'home_assistant'
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = False
REQUIREMENTS = ['influxdb==2.12.0']
REQUIREMENTS = ['influxdb==3.0.0']
CONF_HOST = 'host'
CONF_PORT = 'port'
+1 -2
View File
@@ -39,7 +39,6 @@ def setup(hass, config):
_LOGGER.error("Could not connect to Insteon service.")
return
for component in 'light':
discovery.load_platform(hass, component, DOMAIN, {}, config)
discovery.load_platform(hass, 'light', DOMAIN, {}, config)
return True
+80
View File
@@ -0,0 +1,80 @@
"""
Component for Joaoapps Join services.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/join/
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/nkgilley/python-join-api/archive/'
'3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'joaoapps_join'
CONF_DEVICE_ID = 'device_id'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_API_KEY): cv.string
})
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-locals
def setup(hass, config):
"""Setup Join services."""
from pyjoin import (get_devices, ring_device, set_wallpaper, send_sms,
send_file, send_url, send_notification)
device_id = config[DOMAIN].get(CONF_DEVICE_ID)
api_key = config[DOMAIN].get(CONF_API_KEY)
name = config[DOMAIN].get(CONF_NAME)
if api_key:
if not get_devices(api_key):
_LOGGER.error("Error connecting to Join, check API key")
return False
def ring_service(service):
"""Service to ring devices."""
ring_device(device_id, api_key=api_key)
def set_wallpaper_service(service):
"""Service to set wallpaper on devices."""
set_wallpaper(device_id, url=service.data.get('url'), api_key=api_key)
def send_file_service(service):
"""Service to send files to devices."""
send_file(device_id, url=service.data.get('url'), api_key=api_key)
def send_url_service(service):
"""Service to open url on devices."""
send_url(device_id, url=service.data.get('url'), api_key=api_key)
def send_tasker_service(service):
"""Service to open url on devices."""
send_notification(device_id=device_id,
text=service.data.get('command'),
api_key=api_key)
def send_sms_service(service):
"""Service to send sms from devices."""
send_sms(device_id=device_id,
sms_number=service.data.get('number'),
sms_text=service.data.get('message'),
api_key=api_key)
name = name.lower().replace(" ", "_") + "_" if name else ""
hass.services.register(DOMAIN, name + 'ring', ring_service)
hass.services.register(DOMAIN, name + 'set_wallpaper',
set_wallpaper_service)
hass.services.register(DOMAIN, name + 'send_sms', send_sms_service)
hass.services.register(DOMAIN, name + 'send_file', send_file_service)
hass.services.register(DOMAIN, name + 'send_url', send_url_service)
hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service)
return True
+295
View File
@@ -0,0 +1,295 @@
"""
Support for KNX components.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/knx/
"""
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
DOMAIN = "knx"
REQUIREMENTS = ['knxip==0.3.0']
EVENT_KNX_FRAME_RECEIVED = "knx_frame_received"
CONF_HOST = "host"
CONF_PORT = "port"
DEFAULT_PORT = "3671"
KNXTUNNEL = None
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Setup the connection to the KNX IP interface."""
global KNXTUNNEL
from knxip.ip import KNXIPTunnel
from knxip.core import KNXException
host = config[DOMAIN].get(CONF_HOST, None)
if host is None:
_LOGGER.debug("Will try to auto-detect KNX/IP gateway")
host = "0.0.0.0"
try:
port = int(config[DOMAIN].get(CONF_PORT, DEFAULT_PORT))
except ValueError:
_LOGGER.exception("Can't parse KNX IP interface port")
return False
KNXTUNNEL = KNXIPTunnel(host, port)
try:
KNXTUNNEL.connect()
except KNXException as ex:
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
KNXTUNNEL = None
return False
_LOGGER.info("KNX IP tunnel to %s:%i established", host, port)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_tunnel)
return True
def close_tunnel(_data):
"""Close the NKX tunnel connection on shutdown."""
global KNXTUNNEL
KNXTUNNEL.disconnect()
KNXTUNNEL = None
class KNXConfig(object):
"""Handle the fetching of configuration from the config file."""
def __init__(self, config):
"""Initialize the configuration."""
from knxip.core import parse_group_address
self.config = config
self.should_poll = config.get("poll", True)
self._address = parse_group_address(config.get("address"))
if self.config.get("state_address"):
self._state_address = parse_group_address(
self.config.get("state_address"))
else:
self._state_address = None
@property
def name(self):
"""The name given to the entity."""
return self.config["name"]
@property
def address(self):
"""The address of the device as an integer value.
3 types of addresses are supported:
integer - 0-65535
2 level - a/b
3 level - a/b/c
"""
return self._address
@property
def state_address(self):
"""The group address the device sends its current state to.
Some KNX devices can send the current state to a seperate
group address. This makes send e.g. when an actuator can
be switched but also have a timer functionality.
"""
return self._state_address
class KNXGroupAddress(Entity):
"""Representation of devices connected to a KNX group address."""
def __init__(self, hass, config):
"""Initialize the device."""
self._config = config
self._state = False
self._data = None
_LOGGER.debug("Initalizing KNX group address %s", self.address)
def handle_knx_message(addr, data):
"""Handle an incoming KNX frame.
Handle an incoming frame and update our status if it contains
information relating to this device.
"""
if (addr == self.state_address) or (addr == self.address):
self._state = data
self.update_ha_state()
KNXTUNNEL.register_listener(self.address, handle_knx_message)
if self.state_address:
KNXTUNNEL.register_listener(self.state_address, handle_knx_message)
@property
def name(self):
"""The entity's display name."""
return self._config.name
@property
def config(self):
"""The entity's configuration."""
return self._config
@property
def should_poll(self):
"""Return the state of the polling, if needed."""
return self._config.should_poll
@property
def is_on(self):
"""Return True if the value is not 0 is on, else False."""
if self.should_poll:
self.update()
return self._state != 0
@property
def address(self):
"""Return the KNX group address."""
return self._config.address
@property
def state_address(self):
"""Return the KNX group address."""
return self._config.state_address
@property
def cache(self):
"""The name given to the entity."""
return self._config.config.get("cache", True)
def group_write(self, value):
"""Write to the group address."""
KNXTUNNEL.group_write(self.address, [value])
def update(self):
"""Get the state from KNX bus or cache."""
from knxip.core import KNXException
try:
if self.state_address:
res = KNXTUNNEL.group_read(self.state_address,
use_cache=self.cache)
else:
res = KNXTUNNEL.group_read(self.address,
use_cache=self.cache)
if res:
self._state = res[0]
self._data = res
else:
_LOGGER.debug("Unable to read from KNX address: %s (None)",
self.address)
except KNXException:
_LOGGER.exception("Unable to read from KNX address: %s",
self.address)
return False
class KNXMultiAddressDevice(KNXGroupAddress):
"""Representation of devices connected to a multiple KNX group address.
This is needed for devices like dimmers or shutter actuators as they have
to be controlled by multiple group addresses.
"""
names = {}
values = {}
def __init__(self, hass, config, required, optional=None):
"""Initialize the device.
The namelist argument lists the required addresses. E.g. for a dimming
actuators, the namelist might look like:
onoff_address: 0/0/1
brightness_address: 0/0/2
"""
from knxip.core import parse_group_address, KNXException
super().__init__(self, hass, config)
self.config = config
# parse required addresses
for name in required:
paramname = name + "_address"
addr = self._config.config.get(paramname)
if addr is None:
_LOGGER.exception("Required KNX group address %s missing",
paramname)
raise KNXException("Group address missing in configuration")
addr = parse_group_address(addr)
self.names[addr] = name
# parse optional addresses
for name in optional:
paramname = name + "_address"
addr = self._config.config.get(paramname)
if addr:
try:
addr = parse_group_address(addr)
except KNXException:
_LOGGER.exception("Cannot parse group address %s", addr)
self.names[addr] = name
def handle_frame(frame):
"""Handle an incoming KNX frame.
Handle an incoming frame and update our status if it contains
information relating to this device.
"""
addr = frame.data[0]
if addr in self.names:
self.values[addr] = frame.data[1]
self.update_ha_state()
hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame)
def group_write_address(self, name, value):
"""Write to the group address with the given name."""
KNXTUNNEL.group_write(self.address, [value])
def has_attribute(self, name):
"""Check if the attribute with the given name is defined.
This is mostly important for optional addresses.
"""
for attributename, dummy_attribute in self.names.items():
if attributename == name:
return True
return False
def value(self, name):
"""Return the value to a given named attribute."""
from knxip.core import KNXException
addr = None
for attributename, attributeaddress in self.names.items():
if attributename == name:
addr = attributeaddress
if addr is None:
_LOGGER.exception("Attribute %s undefined", name)
return False
try:
res = KNXTUNNEL.group_read(addr, use_cache=self.cache)
except KNXException:
_LOGGER.exception("Unable to read from KNX address: %s",
addr)
return False
return res
+4 -2
View File
@@ -67,12 +67,13 @@ PROP_TO_ATTR = {
# Service call validation schemas
VALID_TRANSITION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900))
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PROFILE: str,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: cv.byte,
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_COLOR_NAME: str,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
@@ -248,7 +249,8 @@ def setup(hass, config):
class Light(ToggleEntity):
"""Representation of a light."""
# pylint: disable=no-self-use
# pylint: disable=no-self-use, abstract-method
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
+2 -2
View File
@@ -18,7 +18,7 @@ LIGHT_TEMPS = [240, 380]
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup demo light platform."""
"""Setup the demo light platform."""
add_devices_callback([
DemoLight("Bed Light", False),
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
@@ -27,7 +27,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class DemoLight(Light):
"""Provide a demo light."""
"""Represenation of a demo light."""
# pylint: disable=too-many-arguments
def __init__(self, name, state, rgb=None, ct=None, brightness=180):
+1 -2
View File
@@ -4,7 +4,6 @@ Support for EnOcean light sources.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.enocean/
"""
import logging
import math
@@ -86,7 +85,7 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
self._on_state = False
def value_changed(self, val):
"""Update the internal state of this device in HA."""
"""Update the internal state of this device."""
self._brightness = math.floor(val / 100.0 * 256.0)
self._on_state = bool(val != 0)
self.update_ha_state()
+102
View File
@@ -0,0 +1,102 @@
"""
Support for Homematic lighs.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.homematic/
"""
import logging
from homeassistant.components.light import (ATTR_BRIGHTNESS, Light)
from homeassistant.const import STATE_UNKNOWN
import homeassistant.components.homematic as homematic
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematic']
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the Homematic light platform."""
if discovery_info is None:
return
return homematic.setup_hmdevice_discovery_helper(HMLight,
discovery_info,
add_callback_devices)
class HMLight(homematic.HMDevice, Light):
"""Representation of a Homematic light."""
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
if not self.available:
return None
# Is dimmer?
if self._state is "LEVEL":
return int(self._hm_get_state() * 255)
else:
return None
@property
def is_on(self):
"""Return true if light is on."""
try:
return self._hm_get_state() > 0
except TypeError:
return False
def turn_on(self, **kwargs):
"""Turn the light on."""
if not self.available:
return
if ATTR_BRIGHTNESS in kwargs and self._state is "LEVEL":
percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255
self._hmdevice.set_level(percent_bright, self._channel)
else:
self._hmdevice.on(self._channel)
def turn_off(self, **kwargs):
"""Turn the light off."""
if self.available:
self._hmdevice.off(self._channel)
def _check_hm_to_ha_object(self):
"""Check if possible to use the Homematic object as this HA type."""
from pyhomematic.devicetypes.actors import Dimmer, Switch
# Check compatibility from HMDevice
if not super()._check_hm_to_ha_object():
return False
# Check if the Homematic device is correct for this HA device
if isinstance(self._hmdevice, Switch):
return True
if isinstance(self._hmdevice, Dimmer):
return True
_LOGGER.critical("This %s can't be use as light", self._name)
return False
def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata."""
from pyhomematic.devicetypes.actors import Dimmer, Switch
super()._init_data_struct()
# Use STATE
if isinstance(self._hmdevice, Switch):
self._state = "STATE"
# Use LEVEL
if isinstance(self._hmdevice, Dimmer):
self._state = "LEVEL"
# Add state to data dict
if self._state:
_LOGGER.debug("%s init datadict with main node '%s'", self._name,
self._state)
self._data.update({self._state: STATE_UNKNOWN})
else:
_LOGGER.critical("Can't correctly init light %s.", self._name)
+1 -1
View File
@@ -129,7 +129,7 @@ def setup_bridge(host, hass, add_devices_callback, filename,
new_lights = []
api_name = api.get('config').get('name')
if api_name == 'RaspBee-GW':
if api_name in ('RaspBee-GW', 'deCONZ-GW'):
bridge_type = 'deconz'
else:
bridge_type = 'hue'
@@ -4,6 +4,7 @@ Support for LimitlessLED bulbs.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.limitlessled/
"""
# pylint: disable=abstract-method
import logging
from homeassistant.components.light import (
@@ -4,6 +4,7 @@ Support for MySensors lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.mysensors/
"""
# pylint: disable=abstract-method
import logging
from homeassistant.components import mysensors
@@ -1,19 +1,9 @@
"""
Support for Osram Lightify.
Uses: https://github.com/aneumeier/python-lightify for the Osram light
interface.
In order to use the platform just add the following to the configuration.yaml:
light:
platform: osramlightify
host: <hostname_or_ip>
Todo:
Add support for Non RGBW lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.osramlightify/
"""
import logging
import socket
from datetime import timedelta
@@ -40,7 +30,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return lights."""
"""Setup Osram Lightify lights."""
import lightify
host = config.get(CONF_HOST)
if host:
@@ -85,7 +75,7 @@ def setup_bridge(bridge, add_devices_callback):
class OsramLightifyLight(Light):
"""Defines an Osram Lightify Light."""
"""Representation of an Osram Lightify Light."""
def __init__(self, light_id, light, update_lights):
"""Initialize the light."""
+5 -27
View File
@@ -8,12 +8,13 @@ import logging
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
Light, ATTR_RGB_COLOR
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import color as color_util
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
REQUIREMENTS = ['python-wink==0.7.7']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@@ -35,26 +36,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
WinkLight(light) for light in pywink.get_bulbs())
class WinkLight(Light):
class WinkLight(WinkDevice, Light):
"""Representation of a Wink light."""
def __init__(self, wink):
"""
Initialize the light.
:type wink: pywink.devices.standard.bulb.WinkBulb
"""
self.wink = wink
@property
def unique_id(self):
"""Return the ID of this Wink light."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the light if any."""
return self.wink.name()
"""Initialize the Wink device."""
WinkDevice.__init__(self, wink)
@property
def is_on(self):
@@ -66,11 +53,6 @@ class WinkLight(Light):
"""Return the brightness of the light."""
return int(self.wink.brightness() * 255)
@property
def available(self):
"""True if connection == True."""
return self.wink.available
@property
def xy_color(self):
"""Current bulb color in CIE 1931 (XY) color space."""
@@ -112,7 +94,3 @@ class WinkLight(Light):
def turn_off(self):
"""Turn the switch off."""
self.wink.set_state(False)
def update(self):
"""Update state of the light."""
self.wink.update_state(require_desired_state_fulfilled=True)
+212 -6
View File
@@ -4,13 +4,42 @@ Support for Z-Wave lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.zwave/
"""
import logging
# Because we do not compile openzwave on CI
# pylint: disable=import-error
from threading import Timer
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN, Light
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
ATTR_RGB_COLOR, DOMAIN, Light
from homeassistant.components import zwave
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
color_temperature_mired_to_kelvin, color_temperature_to_rgb, \
color_rgb_to_rgbw, color_rgbw_to_rgb
_LOGGER = logging.getLogger(__name__)
AEOTEC = 0x86
AEOTEC_ZW098_LED_BULB = 0x62
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
COLOR_CHANNEL_WARM_WHITE = 0x01
COLOR_CHANNEL_COLD_WHITE = 0x02
COLOR_CHANNEL_RED = 0x04
COLOR_CHANNEL_GREEN = 0x08
COLOR_CHANNEL_BLUE = 0x10
WORKAROUND_ZW098 = 'zw098'
DEVICE_MAPPINGS = {
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
}
# Generate midpoint color temperatures for bulbs that have limited
# support for white light colors
TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -29,7 +58,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return
value.set_change_verified(False)
add_devices([ZwaveDimmer(value)])
if node.has_command_class(zwave.COMMAND_CLASS_COLOR):
try:
add_devices([ZwaveColorLight(value)])
except ValueError as exception:
_LOGGER.warning(
"Error initializing as color bulb: %s "
"Initializing as standard dimmer.", exception)
add_devices([ZwaveDimmer(value)])
else:
add_devices([ZwaveDimmer(value)])
def brightness_state(value):
@@ -50,8 +89,9 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
from pydispatch import dispatcher
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._brightness, self._state = brightness_state(value)
self._brightness = None
self._state = None
self.update_properties()
# Used for value change event handling
self._refreshing = False
@@ -60,6 +100,11 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
dispatcher.connect(
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
def update_properties(self):
"""Update internal properties based on zwave values."""
# Brightness
self._brightness, self._state = brightness_state(self._value)
def _value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id != value.value_id:
@@ -67,7 +112,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
if self._refreshing:
self._refreshing = False
self._brightness, self._state = brightness_state(value)
self.update_properties()
else:
def _refresh_value():
"""Used timer callback for delayed value refresh."""
@@ -108,3 +153,164 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
"""Turn the device off."""
if self._value.node.set_dimmer(self._value.value_id, 0):
self._state = STATE_OFF
def ct_to_rgb(temp):
"""Convert color temperature (mireds) to RGB."""
colorlist = list(
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
return [int(val) for val in colorlist]
class ZwaveColorLight(ZwaveDimmer):
"""Representation of a Z-Wave color changing light."""
def __init__(self, value):
"""Initialize the light."""
self._value_color = None
self._value_color_channels = None
self._color_channels = None
self._rgb = None
self._ct = None
self._zw098 = None
# Here we attempt to find a zwave color value with the same instance
# id as the dimmer value. Currently zwave nodes that change colors
# only include one dimmer and one color command, but this will
# hopefully provide some forward compatibility for new devices that
# have multiple color changing elements.
for value_color in value.node.get_rgbbulbs().values():
if value.instance == value_color.instance:
self._value_color = value_color
if self._value_color is None:
raise ValueError("No matching color command found.")
for value_color_channels in value.node.get_values(
class_id=zwave.COMMAND_CLASS_COLOR, genre='System',
type="Int").values():
self._value_color_channels = value_color_channels
if self._value_color_channels is None:
raise ValueError("Color Channels not found.")
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
self._zw098 = 1
super().__init__(value)
def update_properties(self):
"""Update internal properties based on zwave values."""
super().update_properties()
# Color Channels
self._color_channels = self._value_color_channels.data
# Color Data String
data = self._value_color.data
# RGB is always present in the openzwave color data string.
self._rgb = [
int(data[1:3], 16),
int(data[3:5], 16),
int(data[5:7], 16)]
# Parse remaining color channels. Openzwave appends white channels
# that are present.
index = 7
# Warm white
if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
warm_white = int(data[index:index+2], 16)
index += 2
else:
warm_white = 0
# Cold white
if self._color_channels & COLOR_CHANNEL_COLD_WHITE:
cold_white = int(data[index:index+2], 16)
index += 2
else:
cold_white = 0
# Color temperature. With the AEOTEC ZW098 bulb, only two color
# temperatures are supported. The warm and cold channel values
# indicate brightness for warm/cold color temperature.
if self._zw098:
if warm_white > 0:
self._ct = TEMP_WARM_HASS
self._rgb = ct_to_rgb(self._ct)
elif cold_white > 0:
self._ct = TEMP_COLD_HASS
self._rgb = ct_to_rgb(self._ct)
else:
# RGB color is being used. Just report midpoint.
self._ct = TEMP_MID_HASS
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=warm_white))
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=cold_white))
# If no rgb channels supported, report None.
if not (self._color_channels & COLOR_CHANNEL_RED or
self._color_channels & COLOR_CHANNEL_GREEN or
self._color_channels & COLOR_CHANNEL_BLUE):
self._rgb = None
@property
def rgb_color(self):
"""Return the rgb color."""
return self._rgb
@property
def color_temp(self):
"""Return the color temperature."""
return self._ct
def turn_on(self, **kwargs):
"""Turn the device on."""
rgbw = None
if ATTR_COLOR_TEMP in kwargs:
# Color temperature. With the AEOTEC ZW098 bulb, only two color
# temperatures are supported. The warm and cold channel values
# indicate brightness for warm/cold color temperature.
if self._zw098:
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
self._ct = TEMP_WARM_HASS
rgbw = b'#000000FF00'
else:
self._ct = TEMP_COLD_HASS
rgbw = b'#00000000FF'
elif ATTR_RGB_COLOR in kwargs:
self._rgb = kwargs[ATTR_RGB_COLOR]
if (not self._zw098 and (
self._color_channels & COLOR_CHANNEL_WARM_WHITE or
self._color_channels & COLOR_CHANNEL_COLD_WHITE)):
rgbw = b'#'
for colorval in color_rgb_to_rgbw(*self._rgb):
rgbw += format(colorval, '02x').encode('utf-8')
rgbw += b'00'
else:
rgbw = b'#'
for colorval in self._rgb:
rgbw += format(colorval, '02x').encode('utf-8')
rgbw += b'0000'
if rgbw is None:
_LOGGER.warning("rgbw string was not generated for turn_on")
else:
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
super().turn_on(**kwargs)
+65
View File
@@ -0,0 +1,65 @@
"""
Support for Vera locks.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.vera/
"""
import logging
from homeassistant.components.lock import LockDevice
from homeassistant.const import (
ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED)
from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
DEPENDENCIES = ['vera']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return Vera locks."""
add_devices_callback(
VeraLock(device, VERA_CONTROLLER) for
device in VERA_DEVICES['lock'])
class VeraLock(VeraDevice, LockDevice):
"""Representation of a Vera lock."""
def __init__(self, vera_device, controller):
"""Initialize the Vera device."""
self._state = None
VeraDevice.__init__(self, vera_device, controller)
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
def lock(self, **kwargs):
"""Lock the device."""
self.vera_device.lock()
self._state = STATE_LOCKED
self.update_ha_state()
def unlock(self, **kwargs):
"""Unlock the device."""
self.vera_device.unlock()
self._state = STATE_UNLOCKED
self.update_ha_state()
@property
def is_locked(self):
"""Return true if device is on."""
return self._state == STATE_LOCKED
def update(self):
"""Called by the Vera device callback to update state."""
self._state = (STATE_LOCKED if self.vera_device.is_locked(True)
else STATE_UNLOCKED)
+1 -1
View File
@@ -39,7 +39,7 @@ class VerisureDoorlock(LockDevice):
@property
def name(self):
"""Return the name of the lock."""
return 'Lock {}'.format(self._id)
return '{}'.format(hub.lock_status[self._id].location)
@property
def state(self):
+5 -37
View File
@@ -7,9 +7,10 @@ https://home-assistant.io/components/lock.wink/
import logging
from homeassistant.components.lock import LockDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.7']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -30,38 +31,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(WinkLockDevice(lock) for lock in pywink.get_locks())
class WinkLockDevice(LockDevice):
class WinkLockDevice(WinkDevice, LockDevice):
"""Representation of a Wink lock."""
def __init__(self, wink):
"""Initialize the lock."""
self.wink = wink
self._battery = self.wink.battery_level
@property
def unique_id(self):
"""Return the id of this wink lock."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the lock if any."""
return self.wink.name()
def update(self):
"""Update the state of the lock."""
self.wink.update_state()
WinkDevice.__init__(self, wink)
@property
def is_locked(self):
"""Return true if device is locked."""
return self.wink.state()
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def lock(self, **kwargs):
"""Lock the device."""
self.wink.set_state(True)
@@ -69,16 +50,3 @@ class WinkLockDevice(LockDevice):
def unlock(self, **kwargs):
"""Unlock the device."""
self.wink.set_state(False)
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100
+12 -13
View File
@@ -11,27 +11,23 @@ from itertools import groupby
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
STATE_NOT_HOME, STATE_OFF, STATE_ON)
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
STATE_NOT_HOME, STATE_OFF, STATE_ON)
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.core import State
from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
"""
_LOGGER = logging.getLogger(__name__)
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
@@ -98,11 +94,14 @@ class LogbookView(HomeAssistantView):
else:
start_day = dt_util.start_of_local_day()
start_day = dt_util.as_utc(start_day)
end_day = start_day + timedelta(days=1)
events = recorder.query_events(
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
events = recorder.get_model('Events')
query = recorder.query('Events').filter(
(events.time_fired > start_day) &
(events.time_fired < end_day))
events = recorder.execute(query)
return self.json(humanify(events))
@@ -31,6 +31,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
@@ -75,6 +76,7 @@ SUPPORT_PLAY_MEDIA = 512
SUPPORT_VOLUME_STEP = 1024
SUPPORT_SELECT_SOURCE = 2048
SUPPORT_STOP = 4096
SUPPORT_CLEAR_PLAYLIST = 8192
# simple services that only take entity_id(s) as optional argument
SERVICE_TO_METHOD = {
@@ -89,7 +91,8 @@ SERVICE_TO_METHOD = {
SERVICE_MEDIA_STOP: 'media_stop',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
SERVICE_SELECT_SOURCE: 'select_source'
SERVICE_SELECT_SOURCE: 'select_source',
SERVICE_CLEAR_PLAYLIST: 'clear_playlist'
}
ATTR_TO_PROPERTY = [
@@ -272,6 +275,12 @@ def select_source(hass, source, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data)
def clear_playlist(hass, entity_id=None):
"""Send the media player the command for clear playlist."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data)
def setup(hass, config):
"""Track states and offer events for media_players."""
component = EntityComponent(
@@ -542,6 +551,10 @@ class MediaPlayerDevice(Entity):
"""Select input source."""
raise NotImplementedError()
def clear_playlist(self):
"""Clear players playlist."""
raise NotImplementedError()
# No need to overwrite these.
@property
def support_pause(self):
@@ -588,6 +601,11 @@ class MediaPlayerDevice(Entity):
"""Boolean if select source command supported."""
return bool(self.supported_media_commands & SUPPORT_SELECT_SOURCE)
@property
def support_clear_playlist(self):
"""Boolean if clear playlist command supported."""
return bool(self.supported_media_commands & SUPPORT_CLEAR_PLAYLIST)
def toggle(self):
"""Toggle the power on the media player."""
if self.state in [STATE_OFF, STATE_IDLE]:
@@ -0,0 +1,374 @@
"""
Support for interface with a Sony Bravia TV.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.braviatv/
"""
import logging
import os
import json
import re
from homeassistant.loader import get_component
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
REQUIREMENTS = [
'https://github.com/aparraga/braviarc/archive/0.3.3.zip'
'#braviarc==0.3.3']
BRAVIA_CONFIG_FILE = 'bravia.conf'
CLIENTID_PREFIX = 'HomeAssistant'
NICKNAME = 'Home Assistant'
# Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
def _get_mac_address(ip_address):
"""Get the MAC address of the device."""
from subprocess import Popen, PIPE
pid = Popen(["arp", "-n", ip_address], stdout=PIPE)
pid_component = pid.communicate()[0]
mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
pid_component).groups()[0]
return mac
def _config_from_file(filename, config=None):
"""Create the configuration from a file."""
if config:
# We're writing configuration
bravia_config = _config_from_file(filename)
if bravia_config is None:
bravia_config = {}
new_config = bravia_config.copy()
new_config.update(config)
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(new_config))
except IOError as error:
_LOGGER.error('Saving config file failed: %s', error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except ValueError as error:
return {}
except IOError as error:
_LOGGER.error('Reading config file failed: %s', error)
# This won't work yet
return False
else:
return {}
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Sony Bravia TV platform."""
host = config.get(CONF_HOST)
if host is None:
return # if no host configured, do not continue
pin = None
bravia_config = _config_from_file(hass.config.path(BRAVIA_CONFIG_FILE))
while len(bravia_config):
# Setup a configured TV
host_ip, host_config = bravia_config.popitem()
if host_ip == host:
pin = host_config['pin']
mac = host_config['mac']
name = config.get(CONF_NAME)
add_devices_callback([BraviaTVDevice(host, mac, name, pin)])
return
setup_bravia(config, pin, hass, add_devices_callback)
# pylint: disable=too-many-branches
def setup_bravia(config, pin, hass, add_devices_callback):
"""Setup a Sony Bravia TV based on host parameter."""
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
if name is None:
name = "Sony Bravia TV"
if pin is None:
request_configuration(config, hass, add_devices_callback)
return
else:
mac = _get_mac_address(host)
if mac is not None:
mac = mac.decode('utf8')
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator.request_done(request_id)
_LOGGER.info('Discovery configuration done!')
# Save config
if not _config_from_file(
hass.config.path(BRAVIA_CONFIG_FILE),
{host: {'pin': pin, 'host': host, 'mac': mac}}):
_LOGGER.error('failed to save config file')
add_devices_callback([BraviaTVDevice(host, mac, name, pin)])
def request_configuration(config, hass, add_devices_callback):
"""Request configuration steps from the user."""
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
if name is None:
name = "Sony Bravia"
configurator = get_component('configurator')
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], "Failed to register, please try again.")
return
def bravia_configuration_callback(data):
"""Callback after user enter PIN."""
from braviarc import braviarc
pin = data.get('pin')
braviarc = braviarc.BraviaRC(host)
braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME)
if braviarc.is_connected():
setup_bravia(config, pin, hass, add_devices_callback)
else:
request_configuration(config, hass, add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, name, bravia_configuration_callback,
description='Enter the Pin shown on your Sony Bravia TV.' +
'If no Pin is shown, enter 0000 to let TV show you a Pin.',
description_image="/static/images/smart-tv.png",
submit_caption="Confirm",
fields=[{'id': 'pin', 'name': 'Enter the pin', 'type': ''}]
)
# pylint: disable=abstract-method, too-many-public-methods,
# pylint: disable=too-many-instance-attributes, too-many-arguments
class BraviaTVDevice(MediaPlayerDevice):
"""Representation of a Sony Bravia TV."""
def __init__(self, host, mac, name, pin):
"""Initialize the Sony Bravia device."""
from braviarc import braviarc
self._pin = pin
self._braviarc = braviarc.BraviaRC(host, mac)
self._name = name
self._state = STATE_OFF
self._muted = False
self._program_name = None
self._channel_name = None
self._channel_number = None
self._source = None
self._source_list = []
self._original_content_list = []
self._content_mapping = {}
self._duration = None
self._content_uri = None
self._id = None
self._playing = False
self._start_date_time = None
self._program_media_type = None
self._min_volume = None
self._max_volume = None
self._volume = None
self._braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME)
if self._braviarc.is_connected():
self.update()
else:
self._state = STATE_OFF
def update(self):
"""Update TV info."""
if not self._braviarc.is_connected():
self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME)
if not self._braviarc.is_connected():
return
# Retrieve the latest data.
try:
if self._state == STATE_ON:
# refresh volume info:
self._refresh_volume()
self._refresh_channels()
power_status = self._braviarc.get_power_status()
if power_status == 'active':
self._state = STATE_ON
playing_info = self._braviarc.get_playing_info()
if playing_info is None or len(playing_info) == 0:
self._channel_name = 'App'
else:
self._program_name = playing_info.get('programTitle')
self._channel_name = playing_info.get('title')
self._program_media_type = playing_info.get(
'programMediaType')
self._channel_number = playing_info.get('dispNum')
self._source = playing_info.get('source')
self._content_uri = playing_info.get('uri')
self._duration = playing_info.get('durationSec')
self._start_date_time = playing_info.get('startDateTime')
else:
self._state = STATE_OFF
except Exception as exception_instance: # pylint: disable=broad-except
_LOGGER.error(exception_instance)
self._state = STATE_OFF
def _refresh_volume(self):
"""Refresh volume information."""
volume_info = self._braviarc.get_volume_info()
if volume_info is not None:
self._volume = volume_info.get('volume')
self._min_volume = volume_info.get('minVolume')
self._max_volume = volume_info.get('maxVolume')
self._muted = volume_info.get('mute')
def _refresh_channels(self):
if len(self._source_list) == 0:
self._content_mapping = self._braviarc. \
load_source_list()
self._source_list = []
for key in self._content_mapping:
self._source_list.append(key)
@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 source(self):
"""Return the current input source."""
return self._source
@property
def source_list(self):
"""List of available input sources."""
return self._source_list
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
if self._volume is not None:
return self._volume / 100
else:
return None
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_BRAVIA
@property
def media_title(self):
"""Title of current playing media."""
return_value = None
if self._channel_name is not None:
return_value = self._channel_name
if self._program_name is not None:
return_value = return_value + ': ' + self._program_name
return return_value
@property
def media_content_id(self):
"""Content ID of current playing media."""
return self._channel_name
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._duration
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._braviarc.set_volume_level(volume)
def turn_on(self):
"""Turn the media player on."""
self._braviarc.turn_on()
def turn_off(self):
"""Turn off media player."""
self._braviarc.turn_off()
def volume_up(self):
"""Volume up the media player."""
self._braviarc.volume_up()
def volume_down(self):
"""Volume down media player."""
self._braviarc.volume_down()
def mute_volume(self, mute):
"""Send mute command."""
self._braviarc.mute_volume(mute)
def select_source(self, source):
"""Set the input source."""
if source in self._content_mapping:
uri = self._content_mapping[source]
self._braviarc.play_content(uri)
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._braviarc.media_play()
def media_pause(self):
"""Send media pause command to media player."""
self._playing = False
self._braviarc.media_pause()
def media_next_track(self):
"""Send next track command."""
self._braviarc.media_next_track()
def media_previous_track(self):
"""Send the previous track command."""
self._braviarc.media_previous_track()
@@ -0,0 +1,213 @@
"""
Support for interacting with and controlling the cmus music player.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.cmus/
"""
import logging
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK,
MediaPlayerDevice)
from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
CONF_HOST, CONF_NAME, CONF_PASSWORD,
CONF_PORT)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pycmus==0.1.0']
SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK
def setup_platform(hass, config, add_devices, discover_info=None):
"""Setup the CMUS platform."""
from pycmus import exceptions
host = config.get(CONF_HOST, None)
password = config.get(CONF_PASSWORD, None)
port = config.get(CONF_PORT, None)
name = config.get(CONF_NAME, None)
if host and not password:
_LOGGER.error("A password must be set if using a remote cmus server")
return False
try:
cmus_remote = CmusDevice(host, password, port, name)
except exceptions.InvalidPassword:
_LOGGER.error("The provided password was rejected by cmus")
return False
add_devices([cmus_remote])
class CmusDevice(MediaPlayerDevice):
"""Representation of a running CMUS."""
# pylint: disable=no-member, too-many-public-methods, abstract-method
def __init__(self, server, password, port, name):
"""Initialize the CMUS device."""
from pycmus import remote
if server:
port = port or 3000
self.cmus = remote.PyCmus(server=server, password=password,
port=port)
auto_name = "cmus-%s" % server
else:
self.cmus = remote.PyCmus()
auto_name = "cmus-local"
self._name = name or auto_name
self.status = {}
self.update()
def update(self):
"""Get the latest data and update the state."""
status = self.cmus.get_status_dict()
if not status:
_LOGGER.warning("Recieved no status from cmus")
else:
self.status = status
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the media state."""
if 'status' not in self.status:
self.update()
if self.status['status'] == 'playing':
return STATE_PLAYING
elif self.status['status'] == 'paused':
return STATE_PAUSED
else:
return STATE_OFF
@property
def media_content_id(self):
"""Content ID of current playing media."""
return self.status.get('file')
@property
def content_type(self):
"""Content type of the current playing media."""
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self.status.get('duration')
@property
def media_title(self):
"""Title of current playing media."""
return self.status['tag'].get('title')
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self.status['tag'].get('artist')
@property
def media_track(self):
"""Track number of current playing media, music track only."""
return self.status['tag'].get('tracknumber')
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
return self.status['tag'].get('album')
@property
def media_album_artist(self):
"""Album artist of current playing media, music track only."""
return self.status['tag'].get('albumartist')
@property
def volume_level(self):
"""Return the volume level."""
left = self.status['set'].get('vol_left')[0]
right = self.status['set'].get('vol_right')[0]
if left != right:
volume = float(left + right) / 2
else:
volume = left
return int(volume)/100
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_CMUS
def turn_off(self):
"""Service to send the CMUS the command to stop playing."""
self.cmus.player_stop()
def turn_on(self):
"""Service to send the CMUS the command to start playing."""
self.cmus.player_play()
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self.cmus.set_volume(int(volume * 100))
def volume_up(self):
"""Function to send CMUS the command for volume up."""
left = self.status['set'].get('vol_left')
right = self.status['set'].get('vol_right')
if left != right:
current_volume = float(left + right) / 2
else:
current_volume = left
if current_volume <= 100:
self.cmus.set_volume(int(current_volume) + 5)
def volume_down(self):
"""Function to send CMUS the command for volume down."""
left = self.status['set'].get('vol_left')
right = self.status['set'].get('vol_right')
if left != right:
current_volume = float(left + right) / 2
else:
current_volume = left
if current_volume <= 100:
self.cmus.set_volume(int(current_volume) - 5)
def play_media(self, media_type, media_id, **kwargs):
"""Send the play command."""
if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]:
self.cmus.player_play_file(media_id)
else:
_LOGGER.error(
"Invalid media type %s. Only %s and %s are supported",
media_type, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST)
def media_pause(self):
"""Send the pause command."""
self.cmus.player_pause()
def media_next_track(self):
"""Send next track command."""
self.cmus.player_next()
def media_previous_track(self):
"""Send next track command."""
self.cmus.player_prev()
def media_seek(self, position):
"""Send seek command."""
self.cmus.seek(position)
def media_play(self):
"""Send the play command."""
self.cmus.player_play()
def media_stop(self):
"""Send the stop command."""
self.cmus.stop()
+11 -4
View File
@@ -8,7 +8,7 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
@@ -32,7 +32,7 @@ YOUTUBE_PLAYER_SUPPORT = \
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST
NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
@@ -214,12 +214,12 @@ class DemoMusicPlayer(AbstractDemoPlayer):
@property
def media_title(self):
"""Return the title of current playing media."""
return self.tracks[self._cur_track][1]
return self.tracks[self._cur_track][1] if len(self.tracks) > 0 else ""
@property
def media_artist(self):
"""Return the artist of current playing media (Music track only)."""
return self.tracks[self._cur_track][0]
return self.tracks[self._cur_track][0] if len(self.tracks) > 0 else ""
@property
def media_album_name(self):
@@ -257,6 +257,13 @@ class DemoMusicPlayer(AbstractDemoPlayer):
self._cur_track += 1
self.update_ha_state()
def clear_playlist(self):
"""Clear players playlist."""
self.tracks = []
self._cur_track = 0
self._player_state = STATE_OFF
self.update_ha_state()
class DemoTVShowPlayer(AbstractDemoPlayer):
"""A Demo media player that only supports YouTube."""
@@ -15,7 +15,7 @@ from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['websocket-client==0.35.0']
REQUIREMENTS = ['websocket-client==0.37.0']
SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
@@ -15,7 +15,7 @@ from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['jsonrpc-requests==0.2']
REQUIREMENTS = ['jsonrpc-requests==0.3']
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
@@ -301,4 +301,4 @@ class KodiDevice(MediaPlayerDevice):
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}, {})
self._server.Player.Open({"item": {"file": str(media_id)}})
+11 -7
View File
@@ -122,7 +122,11 @@ def setup_plexserver(host, token, hass, add_devices_callback):
try:
devices = plexserver.clients()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex devices")
_LOGGER.exception('Error listing plex devices')
return
except OSError:
_LOGGER.error(
'Could not connect to plex server at http://%s', host)
return
new_plex_clients = []
@@ -148,7 +152,7 @@ def setup_plexserver(host, token, hass, add_devices_callback):
try:
sessions = plexserver.sessions()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex sessions")
_LOGGER.exception('Error listing plex sessions')
return
plex_sessions.clear()
@@ -166,7 +170,7 @@ def request_configuration(host, hass, add_devices_callback):
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], "Failed to register, please try again.")
_CONFIGURING[host], 'Failed to register, please try again.')
return
@@ -175,10 +179,10 @@ def request_configuration(host, hass, add_devices_callback):
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, "Plex Media Server", plex_configuration_callback,
hass, 'Plex Media Server', plex_configuration_callback,
description=('Enter the X-Plex-Token'),
description_image="/static/images/config_plex_mediaserver.png",
submit_caption="Confirm",
description_image='/static/images/config_plex_mediaserver.png',
submit_caption='Confirm',
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}]
)
@@ -201,7 +205,7 @@ class PlexClient(MediaPlayerDevice):
@property
def unique_id(self):
"""Return the id of this plex client."""
return "{}.{}".format(
return '{}.{}'.format(
self.__class__, self.device.machineIdentifier or self.device.name)
@property
+17 -7
View File
@@ -4,7 +4,6 @@ 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 (
@@ -16,8 +15,8 @@ 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']
'https://github.com/bah2830/python-roku/archive/3.1.2.zip'
'#roku==3.1.2']
KNOWN_HOSTS = []
DEFAULT_PORT = 8060
@@ -46,8 +45,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rokus = []
for host in hosts:
rokus.append(RokuDevice(host))
KNOWN_HOSTS.append(host)
new_roku = RokuDevice(host)
if new_roku.name is None:
_LOGGER.error("Unable to initialize roku at %s", host)
else:
rokus.append(RokuDevice(host))
KNOWN_HOSTS.append(host)
add_devices(rokus)
@@ -62,6 +66,11 @@ class RokuDevice(MediaPlayerDevice):
from roku import Roku
self.roku = Roku(host)
self.roku_name = None
self.ip_address = host
self.channels = []
self.current_app = None
self.update()
def update(self):
@@ -77,8 +86,9 @@ class RokuDevice(MediaPlayerDevice):
self.current_app = self.roku.current_app
else:
self.current_app = None
except requests.exceptions.ConnectionError:
self.current_app = None
except (requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout):
_LOGGER.error("Unable to connect to roku at %s", self.ip_address)
def get_source_list(self):
"""Get the list of applications to be used as sources."""
@@ -146,6 +146,14 @@ select_source:
description: Name of the source to switch to. Platform dependent.
example: 'video1'
clear_playlist:
description: Send the media player the command to clear players playlist.
fields:
entity_id:
description: Name(s) of entites to change source on
example: 'media_player.living_room_chromecast'
sonos_group_players:
description: Send Sonos media player the command for grouping all players into one (party mode).
@@ -154,12 +162,20 @@ sonos_group_players:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos'
sonos_unjoin:
description: Unjoin the player from a group.
fields:
entity_id:
description: Name(s) of entites that will be unjoined from their group. Platform dependent.
example: 'media_player.living_room_sonos'
sonos_snapshot:
description: Take a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
description: Name(s) of entites that will be snapshot. Platform dependent.
example: 'media_player.living_room_sonos'
sonos_restore:
@@ -167,5 +183,5 @@ sonos_restore:
fields:
entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos'
description: Name(s) of entites that will be restored. Platform dependent.
example: 'media_player.living_room_sonos'
@@ -4,7 +4,6 @@ Support for interacting with Snapcast clients.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.snapcast/
"""
import logging
import socket
+85 -43
View File
@@ -12,7 +12,8 @@ from os import path
from homeassistant.components.media_player import (
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)
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
from homeassistant.config import load_yaml_config_file
@@ -31,14 +32,19 @@ _REQUESTS_LOGGER.setLevel(logging.ERROR)
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
SUPPORT_SEEK
SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SELECT_SOURCE
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
SERVICE_UNJOIN = 'sonos_unjoin'
SERVICE_SNAPSHOT = 'sonos_snapshot'
SERVICE_RESTORE = 'sonos_restore'
SUPPORT_SOURCE_LINEIN = 'Line-in'
SUPPORT_SOURCE_TV = 'TV'
SUPPORT_SOURCE_RADIO = 'Radio'
# pylint: disable=unused-argument
# pylint: disable=unused-argument, too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Sonos platform."""
import soco
@@ -72,47 +78,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices)
_LOGGER.info('Added %s Sonos speakers', len(players))
def _apply_service(service, service_func, *service_func_args):
"""Internal func for applying a service."""
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:
service_func(device, *service_func_args)
device.update_ha_state(True)
def group_players_service(service):
"""Group media players, use player as coordinator."""
entity_id = service.data.get('entity_id')
_apply_service(service, SonosDevice.group_players)
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
def unjoin_service(service):
"""Unjoin the player from a group."""
_apply_service(service, SonosDevice.unjoin)
for device in _devices:
device.group_players()
device.update_ha_state(True)
def snapshot(service):
def snapshot_service(service):
"""Take a snapshot."""
entity_id = service.data.get('entity_id')
_apply_service(service, SonosDevice.snapshot)
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
device.snapshot(service)
device.update_ha_state(True)
def restore(service):
def restore_service(service):
"""Restore a snapshot."""
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.restore(service)
device.update_ha_state(True)
_apply_service(service, SonosDevice.restore)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
@@ -121,12 +115,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS))
hass.services.register(DOMAIN, SERVICE_UNJOIN,
unjoin_service,
descriptions.get(SERVICE_UNJOIN))
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
snapshot,
snapshot_service,
descriptions.get(SERVICE_SNAPSHOT))
hass.services.register(DOMAIN, SERVICE_RESTORE,
restore,
restore_service,
descriptions.get(SERVICE_RESTORE))
return True
@@ -169,12 +167,12 @@ class SonosDevice(MediaPlayerDevice):
# pylint: disable=too-many-arguments
def __init__(self, hass, player):
"""Initialize the Sonos device."""
from soco.snapshot import Snapshot
self.hass = hass
self.volume_increment = 5
super(SonosDevice, self).__init__()
self._player = player
self.update()
from soco.snapshot import Snapshot
self.soco_snapshot = Snapshot(self._player)
@property
@@ -268,6 +266,10 @@ class SonosDevice(MediaPlayerDevice):
@property
def media_title(self):
"""Title of current playing media."""
if self._player.is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
if self._player.is_playing_tv:
return SUPPORT_SOURCE_TV
if 'artist' in self._trackinfo and 'title' in self._trackinfo:
return '{artist} - {title}'.format(
artist=self._trackinfo['artist'],
@@ -297,6 +299,36 @@ class SonosDevice(MediaPlayerDevice):
"""Mute (true) or unmute (false) media player."""
self._player.mute = mute
def select_source(self, source):
"""Select input source."""
if source == SUPPORT_SOURCE_LINEIN:
self._player.switch_to_line_in()
elif source == SUPPORT_SOURCE_TV:
self._player.switch_to_tv()
@property
def source_list(self):
"""List of available input sources."""
source = []
# generate list of supported device
source.append(SUPPORT_SOURCE_LINEIN)
source.append(SUPPORT_SOURCE_TV)
source.append(SUPPORT_SOURCE_RADIO)
return source
@property
def source(self):
"""Name of the current input source."""
if self._player.is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
if self._player.is_playing_tv:
return SUPPORT_SOURCE_TV
if self._player.is_playing_radio:
return SUPPORT_SOURCE_RADIO
return None
@only_if_coordinator
def turn_off(self):
"""Turn off media player."""
@@ -327,6 +359,11 @@ class SonosDevice(MediaPlayerDevice):
"""Send seek command."""
self._player.seek(str(datetime.timedelta(seconds=int(position))))
@only_if_coordinator
def clear_playlist(self):
"""Clear players playlist."""
self._player.clear_queue()
@only_if_coordinator
def turn_on(self):
"""Turn the media player on."""
@@ -356,12 +393,17 @@ class SonosDevice(MediaPlayerDevice):
self._player.partymode()
@only_if_coordinator
def snapshot(self, service):
def unjoin(self):
"""Unjoin the player from a group."""
self._player.unjoin()
@only_if_coordinator
def snapshot(self):
"""Snapshot the player."""
self.soco_snapshot.snapshot()
@only_if_coordinator
def restore(self, service):
def restore(self):
"""Restore snapshot for the player."""
self.soco_snapshot.restore(True)
@@ -4,7 +4,6 @@ Combination of multiple media players into one for a universal controller.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.universal/
"""
import logging
# pylint: disable=import-error
from copy import copy
@@ -18,8 +17,9 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED,
ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, ATTR_INPUT_SOURCE,
SERVICE_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
ATTR_INPUT_SOURCE, SERVICE_SELECT_SOURCE, SERVICE_CLEAR_PLAYLIST,
MediaPlayerDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
@@ -347,9 +347,12 @@ class UniversalMediaPlayer(MediaPlayerDevice):
ATTR_MEDIA_VOLUME_MUTED in self._attrs:
flags |= SUPPORT_VOLUME_MUTE
if SUPPORT_SELECT_SOURCE in self._cmds:
if SERVICE_SELECT_SOURCE in self._cmds:
flags |= SUPPORT_SELECT_SOURCE
if SERVICE_CLEAR_PLAYLIST in self._cmds:
flags |= SUPPORT_CLEAR_PLAYLIST
return flags
@property
@@ -425,6 +428,10 @@ class UniversalMediaPlayer(MediaPlayerDevice):
data = {ATTR_INPUT_SOURCE: source}
self._call_service(SERVICE_SELECT_SOURCE, data)
def clear_playlist(self):
"""Clear players playlist."""
self._call_service(SERVICE_CLEAR_PLAYLIST)
def update(self):
"""Update state in HA."""
for child_name in self._children:
+1 -1
View File
@@ -29,7 +29,7 @@ MQTT_CLIENT = None
SERVICE_PUBLISH = 'publish'
EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received'
REQUIREMENTS = ['paho-mqtt==1.1']
REQUIREMENTS = ['paho-mqtt==1.2']
CONF_EMBEDDED = 'embedded'
CONF_BROKER = 'broker'
+1 -1
View File
@@ -38,7 +38,7 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_MESSAGE): cv.template,
vol.Optional(ATTR_TITLE, default=ATTR_TITLE_DEFAULT): cv.string,
vol.Optional(ATTR_TARGET): cv.string,
vol.Optional(ATTR_DATA): dict, # nobody seems to be using this (yet)
vol.Optional(ATTR_DATA): dict,
})
_LOGGER = logging.getLogger(__name__)
@@ -0,0 +1,62 @@
"""
Join platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.join/
"""
import logging
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_PLATFORM, CONF_NAME, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/nkgilley/python-join-api/archive/'
'3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1']
_LOGGER = logging.getLogger(__name__)
CONF_DEVICE_ID = 'device_id'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'joaoapps_join',
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_API_KEY): cv.string
})
# pylint: disable=unused-variable
def get_service(hass, config):
"""Get the Join notification service."""
device_id = config.get(CONF_DEVICE_ID)
api_key = config.get(CONF_API_KEY)
if api_key:
from pyjoin import get_devices
if not get_devices(api_key):
_LOGGER.error("Error connecting to Join, check API key")
return False
return JoinNotificationService(device_id, api_key)
# pylint: disable=too-few-public-methods
class JoinNotificationService(BaseNotificationService):
"""Implement the notification service for Join."""
def __init__(self, device_id, api_key=None):
"""Initialize the service."""
self._device_id = device_id
self._api_key = api_key
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
from pyjoin import send_notification
title = kwargs.get(ATTR_TITLE)
data = kwargs.get(ATTR_DATA) or {}
send_notification(device_id=self._device_id,
text=message,
title=title,
icon=data.get('icon'),
smallicon=data.get('smallicon'),
api_key=self._api_key)
+13 -2
View File
@@ -7,7 +7,7 @@ https://home-assistant.io/components/notify.pushover/
import logging
from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService)
ATTR_TITLE, ATTR_TARGET, ATTR_DATA, DOMAIN, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
@@ -51,7 +51,18 @@ class PushoverNotificationService(BaseNotificationService):
"""Send a message to a user."""
from pushover import RequestError
# Make a copy and use empty dict if necessary
data = dict(kwargs.get(ATTR_DATA) or {})
data['title'] = kwargs.get(ATTR_TITLE)
target = kwargs.get(ATTR_TARGET)
if target is not None:
data['device'] = target
try:
self.pushover.send_message(message, title=kwargs.get(ATTR_TITLE))
self.pushover.send_message(message, **data)
except ValueError as val_err:
_LOGGER.error(str(val_err))
except RequestError:
_LOGGER.exception("Could not send pushover notification")
@@ -13,3 +13,7 @@ notify:
target:
description: Target of the notification. Optional depending on the platform
example: platform specific
data:
description: Extended information for notification. Optional depending on the platform
example: platform specific
+2 -3
View File
@@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
REQUIREMENTS = ['slacker==0.9.16']
REQUIREMENTS = ['slacker==0.9.21']
_LOGGER = logging.getLogger(__name__)
@@ -30,8 +30,7 @@ def get_service(hass, config):
config[CONF_API_KEY])
except slacker.Error:
_LOGGER.exception(
"Slack authentication failed")
_LOGGER.exception("Slack authentication failed")
return None
+54 -2
View File
@@ -4,17 +4,27 @@ Telegram platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.telegram/
"""
import io
import logging
import urllib
import requests
from requests.auth import HTTPBasicAuth
from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService)
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-telegram-bot==4.2.1']
REQUIREMENTS = ['python-telegram-bot==4.3.3']
ATTR_PHOTO = "photo"
ATTR_FILE = "file"
ATTR_URL = "url"
ATTR_CAPTION = "caption"
ATTR_USERNAME = "username"
ATTR_PASSWORD = "password"
def get_service(hass, config):
@@ -54,9 +64,51 @@ class TelegramNotificationService(BaseNotificationService):
import telegram
title = kwargs.get(ATTR_TITLE)
data = kwargs.get(ATTR_DATA, {})
# send message
try:
self.bot.sendMessage(chat_id=self._chat_id,
text=title + " " + message)
except telegram.error.TelegramError:
_LOGGER.exception("Error sending message.")
return
# send photo
if ATTR_PHOTO in data:
# if not a list
if not isinstance(data[ATTR_PHOTO], list):
photos = [data[ATTR_PHOTO]]
else:
photos = data[ATTR_PHOTO]
try:
for photo_data in photos:
caption = photo_data.get(ATTR_CAPTION, None)
# file is a url
if ATTR_URL in photo_data:
# use http authenticate
if ATTR_USERNAME in photo_data and\
ATTR_PASSWORD in photo_data:
req = requests.get(
photo_data[ATTR_URL],
auth=HTTPBasicAuth(photo_data[ATTR_USERNAME],
photo_data[ATTR_PASSWORD])
)
else:
req = requests.get(photo_data[ATTR_URL])
file_id = io.BytesIO(req.content)
elif ATTR_FILE in photo_data:
file_id = open(photo_data[ATTR_FILE], "rb")
else:
_LOGGER.error("No url or path is set for photo!")
continue
self.bot.sendPhoto(chat_id=self._chat_id,
photo=file_id, caption=caption)
except (OSError, IOError, telegram.error.TelegramError,
urllib.error.HTTPError):
_LOGGER.exception("Error sending photo.")
return
@@ -0,0 +1,85 @@
"""
A component which is collecting configuration errors.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/persistent_notification/
"""
import os
import logging
import voluptuous as vol
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template, config_validation as cv
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file
DOMAIN = 'persistent_notification'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_CREATE = 'create'
ATTR_TITLE = 'title'
ATTR_MESSAGE = 'message'
ATTR_NOTIFICATION_ID = 'notification_id'
SCHEMA_SERVICE_CREATE = vol.Schema({
vol.Required(ATTR_MESSAGE): cv.template,
vol.Optional(ATTR_TITLE): cv.template,
vol.Optional(ATTR_NOTIFICATION_ID): cv.string,
})
DEFAULT_OBJECT_ID = 'notification'
_LOGGER = logging.getLogger(__name__)
def create(hass, message, title=None, notification_id=None):
"""Generate a notification."""
data = {
key: value for key, value in [
(ATTR_TITLE, title),
(ATTR_MESSAGE, message),
(ATTR_NOTIFICATION_ID, notification_id),
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_CREATE, data)
def setup(hass, config):
"""Setup the persistent notification component."""
def create_service(call):
"""Handle a create notification service call."""
title = call.data.get(ATTR_TITLE)
message = call.data.get(ATTR_MESSAGE)
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
if notification_id is not None:
entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
else:
entity_id = generate_entity_id(ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID,
hass=hass)
attr = {}
if title is not None:
try:
title = template.render(hass, title)
except TemplateError as ex:
_LOGGER.error('Error rendering title %s: %s', title, ex)
attr[ATTR_TITLE] = title
try:
message = template.render(hass, message)
except TemplateError as ex:
_LOGGER.error('Error rendering message %s: %s', message, ex)
hass.states.set(entity_id, message, attr)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_CREATE, create_service,
descriptions[DOMAIN][SERVICE_CREATE],
SCHEMA_SERVICE_CREATE)
return True
-529
View File
@@ -1,529 +0,0 @@
"""
Support for recording details.
Component that records all events and state changes. Allows other components
to query this database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/recorder/
"""
import atexit
import json
import logging
import queue
import sqlite3
import threading
from datetime import date, datetime, timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
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"
DB_FILE = 'home-assistant.db'
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__)
def query(sql_query, arguments=None):
"""Query the database."""
_verify_instance()
return _INSTANCE.query(sql_query, arguments)
def query_states(state_query, arguments=None):
"""Query the database and return a list of states."""
return [
row for row in
(row_to_state(row) for row in query(state_query, arguments))
if row is not None]
def query_events(event_query, arguments=None):
"""Query the database and return a list of states."""
return [
row for row in
(row_to_event(row) for row in query(event_query, arguments))
if row is not None]
def row_to_state(row):
"""Convert a database row to a state."""
try:
return State(
row[1], row[2], json.loads(row[3]),
dt_util.utc_from_timestamp(row[4]),
dt_util.utc_from_timestamp(row[5]))
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to state: %s", row)
return None
def row_to_event(row):
"""Convert a databse row to an event."""
try:
return Event(row[1], json.loads(row[2]), EventOrigin(row[3]),
dt_util.utc_from_timestamp(row[5]))
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to event: %s", row)
return None
def run_information(point_in_time=None):
"""Return information about current run.
There is also the run that covers point_in_time.
"""
_verify_instance()
if point_in_time is None or point_in_time > _INSTANCE.recording_start:
return RecorderRun()
run = _INSTANCE.query(
"SELECT * FROM recorder_runs WHERE start<? AND END>?",
(point_in_time, point_in_time), return_value=RETURN_ONE_ROW)
return RecorderRun(run) if run else None
def setup(hass, config):
"""Setup the recorder."""
# pylint: disable=global-statement
global _INSTANCE
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
_INSTANCE = Recorder(hass, purge_days=purge_days)
return True
class RecorderRun(object):
"""Representation of a recorder run."""
def __init__(self, row=None):
"""Initialize the recorder run."""
self.end = None
if row is None:
self.start = _INSTANCE.recording_start
self.closed_incorrect = False
else:
self.start = dt_util.utc_from_timestamp(row[1])
if row[2] is not None:
self.end = dt_util.utc_from_timestamp(row[2])
self.closed_incorrect = bool(row[3])
def entity_ids(self, point_in_time=None):
"""Return the entity ids that existed in this run.
Specify point_in_time if you want to know which existed at that point
in time inside the run.
"""
where = self.where_after_start_run
where_data = []
if point_in_time is not None or self.end is not None:
where += "AND created < ? "
where_data.append(point_in_time or self.end)
return [row[0] for row in query(
"SELECT entity_id FROM states WHERE {}"
"GROUP BY entity_id".format(where), where_data)]
@property
def where_after_start_run(self):
"""Return SQL WHERE clause.
Selection of the rows created after the start of the run.
"""
return "created >= {} ".format(_adapt_datetime(self.start))
@property
def where_limit_to_run(self):
"""Return a SQL WHERE clause.
For limiting the results to this run.
"""
where = self.where_after_start_run
if self.end is not None:
where += "AND created < {} ".format(_adapt_datetime(self.end))
return where
class Recorder(threading.Thread):
"""A threaded recorder class."""
# pylint: disable=too-many-instance-attributes
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()
self.lock = threading.Lock()
self.recording_start = dt_util.utcnow()
self.utc_offset = dt_util.now().utcoffset().total_seconds()
self.db_path = self.hass.config.path(DB_FILE)
def start_recording(event):
"""Start recording."""
self.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_recording)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
hass.bus.listen(MATCH_ALL, self.event_listener)
def run(self):
"""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()
if event == self.quit_object:
self._close_run()
self._close_connection()
self.queue.task_done()
return
elif event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done()
continue
event_id = self.record_event(event)
if event.event_type == EVENT_STATE_CHANGED:
self.record_state(
event.data['entity_id'], event.data.get('new_state'),
event_id)
self.queue.task_done()
def event_listener(self, event):
"""Listen for new events and put them in the process queue."""
self.queue.put(event)
def shutdown(self, event):
"""Tell the recorder to shut down."""
self.queue.put(self.quit_object)
self.block_till_done()
def record_state(self, entity_id, state, event_id):
"""Save a state to the database."""
now = dt_util.utcnow()
# State got deleted
if state is None:
state_state = ''
state_domain = ''
state_attr = '{}'
last_changed = last_updated = now
else:
state_domain = state.domain
state_state = state.state
state_attr = json.dumps(dict(state.attributes))
last_changed = state.last_changed
last_updated = state.last_updated
info = (
entity_id, state_domain, state_state, state_attr,
last_changed, last_updated,
now, self.utc_offset, event_id)
self.query(
"""
INSERT INTO states (
entity_id, domain, state, attributes, last_changed, last_updated,
created, utc_offset, event_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
info)
def record_event(self, event):
"""Save an event to the database."""
info = (
event.event_type, json.dumps(event.data, cls=JSONEncoder),
str(event.origin), dt_util.utcnow(), event.time_fired,
self.utc_offset
)
return self.query(
"INSERT INTO events ("
"event_type, event_data, origin, created, time_fired, utc_offset"
") VALUES (?, ?, ?, ?, ?, ?)", info, RETURN_LASTROWID)
def query(self, sql_query, data=None, return_value=None):
"""Query the database."""
try:
with self.conn, self.lock:
_LOGGER.debug("Running query %s", sql_query)
cur = self.conn.cursor()
if data is not None:
cur.execute(sql_query, data)
else:
cur.execute(sql_query)
if return_value == RETURN_ROWCOUNT:
return cur.rowcount
elif return_value == RETURN_LASTROWID:
return cur.lastrowid
elif return_value == RETURN_ONE_ROW:
return cur.fetchone()
else:
return cur.fetchall()
except (sqlite3.IntegrityError, sqlite3.OperationalError,
sqlite3.ProgrammingError):
_LOGGER.exception(
"Error querying the database using: %s", sql_query)
return []
def block_till_done(self):
"""Block till all events processed."""
self.queue.join()
def _setup_connection(self):
"""Ensure database is ready to fly."""
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row
# Make sure the database is closed whenever Python exits
# without the STOP event being fired.
atexit.register(self._close_connection)
# Have datetime objects be saved as integers.
sqlite3.register_adapter(date, _adapt_datetime)
sqlite3.register_adapter(datetime, _adapt_datetime)
# Validate we are on the correct schema or that we have to migrate.
cur = self.conn.cursor()
def save_migration(migration_id):
"""Save and commit a migration to the database."""
cur.execute('INSERT INTO schema_version VALUES (?, ?)',
(migration_id, dt_util.utcnow()))
self.conn.commit()
_LOGGER.info("Database migrated to version %d", migration_id)
try:
cur.execute('SELECT max(migration_id) FROM schema_version;')
migration_id = cur.fetchone()[0] or 0
except sqlite3.OperationalError:
# The table does not exist.
cur.execute('CREATE TABLE schema_version ('
'migration_id integer primary key, performed integer)')
migration_id = 0
if migration_id < 1:
cur.execute("""
CREATE TABLE recorder_runs (
run_id integer primary key,
start integer,
end integer,
closed_incorrect integer default 0,
created integer)
""")
cur.execute("""
CREATE TABLE events (
event_id integer primary key,
event_type text,
event_data text,
origin text,
created integer)
""")
cur.execute(
'CREATE INDEX events__event_type ON events(event_type)')
cur.execute("""
CREATE TABLE states (
state_id integer primary key,
entity_id text,
state text,
attributes text,
last_changed integer,
last_updated integer,
created integer)
""")
cur.execute('CREATE INDEX states__entity_id ON states(entity_id)')
save_migration(1)
if migration_id < 2:
cur.execute("""
ALTER TABLE events
ADD COLUMN time_fired integer
""")
cur.execute('UPDATE events SET time_fired=created')
save_migration(2)
if migration_id < 3:
utc_offset = self.utc_offset
cur.execute("""
ALTER TABLE recorder_runs
ADD COLUMN utc_offset integer
""")
cur.execute("""
ALTER TABLE events
ADD COLUMN utc_offset integer
""")
cur.execute("""
ALTER TABLE states
ADD COLUMN utc_offset integer
""")
cur.execute("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
cur.execute("UPDATE events SET utc_offset=?", [utc_offset])
cur.execute("UPDATE states SET utc_offset=?", [utc_offset])
save_migration(3)
if migration_id < 4:
# We had a bug where we did not save utc offset for recorder runs.
cur.execute(
"""UPDATE recorder_runs SET utc_offset=?
WHERE utc_offset IS NULL""", [self.utc_offset])
cur.execute("""
ALTER TABLE states
ADD COLUMN event_id integer
""")
save_migration(4)
if migration_id < 5:
# Add domain so that thermostat graphs look right.
try:
cur.execute("""
ALTER TABLE states
ADD COLUMN domain text
""")
except sqlite3.OperationalError:
# We had a bug in this migration for a while on dev.
# Without this, dev-users will have to throw away their db.
pass
# TravisCI has Python compiled against an old version of SQLite3
# which misses the instr method.
self.conn.create_function(
"instr", 2,
lambda string, substring: string.find(substring) + 1)
# Populate domain with defaults.
cur.execute("""
UPDATE states
set domain=substr(entity_id, 0, instr(entity_id, '.'))
""")
# Add indexes we are going to use a lot on selects.
cur.execute("""
CREATE INDEX states__state_changes ON
states (last_changed, last_updated, entity_id)""")
cur.execute("""
CREATE INDEX states__significant_changes ON
states (domain, last_updated, entity_id)""")
save_migration(5)
def _close_connection(self):
"""Close connection to the database."""
_LOGGER.info("Closing database")
atexit.unregister(self._close_connection)
self.conn.close()
def _setup_run(self):
"""Log the start of the current run."""
if self.query("""UPDATE recorder_runs SET end=?, closed_incorrect=1
WHERE end IS NULL""", (self.recording_start, ),
return_value=RETURN_ROWCOUNT):
_LOGGER.warning("Found unfinished sessions")
self.query(
"""INSERT INTO recorder_runs (start, created, utc_offset)
VALUES (?, ?, ?)""",
(self.recording_start, dt_util.utcnow(), self.utc_offset))
def _close_run(self):
"""Save end time for current run."""
self.query(
"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."""
return dt_util.as_utc(datetimestamp).timestamp()
def _verify_instance():
"""Throw error if recorder not initialized."""
if _INSTANCE is None:
raise RuntimeError("Recorder not initialized.")
@@ -0,0 +1,337 @@
"""
Support for recording details.
Component that records all events and state changes. Allows other components
to query this database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/recorder/
"""
import logging
import queue
import threading
import time
from datetime import timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED, MATCH_ALL)
from homeassistant.helpers.event import track_point_in_utc_time
DOMAIN = "recorder"
REQUIREMENTS = ['sqlalchemy==1.0.14']
DEFAULT_URL = "sqlite:///{hass_config_path}"
DEFAULT_DB_FILE = "home-assistant_v2.db"
CONF_DB_URL = "db_url"
CONF_PURGE_DAYS = "purge_days"
RETRIES = 3
CONNECT_RETRY_WAIT = 10
QUERY_RETRY_WAIT = 0.1
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
vol.Range(min=1)),
vol.Optional(CONF_DB_URL): vol.Url(''),
})
}, extra=vol.ALLOW_EXTRA)
_INSTANCE = None
_LOGGER = logging.getLogger(__name__)
# These classes will be populated during setup()
# pylint: disable=invalid-name
Session = None
def execute(q):
"""Query the database and convert the objects to HA native form.
This method also retries a few times in the case of stale connections.
"""
import sqlalchemy.exc
for _ in range(0, RETRIES):
try:
return [
row for row in
(row.to_native() for row in q)
if row is not None]
except sqlalchemy.exc.SQLAlchemyError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
return []
def run_information(point_in_time=None):
"""Return information about current run.
There is also the run that covers point_in_time.
"""
_verify_instance()
recorder_runs = get_model('RecorderRuns')
if point_in_time is None or point_in_time > _INSTANCE.recording_start:
return recorder_runs(
end=None,
start=_INSTANCE.recording_start,
closed_incorrect=False)
return query('RecorderRuns').filter(
(recorder_runs.start < point_in_time) &
(recorder_runs.end > point_in_time)).first()
def setup(hass, config):
"""Setup the recorder."""
# pylint: disable=global-statement
# pylint: disable=too-many-locals
global _INSTANCE
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None)
if not db_url:
db_url = DEFAULT_URL.format(
hass_config_path=hass.config.path(DEFAULT_DB_FILE))
_INSTANCE = Recorder(hass, purge_days=purge_days, uri=db_url)
return True
def query(model_name, *args):
"""Helper to return a query handle."""
if isinstance(model_name, str):
return Session().query(get_model(model_name), *args)
return Session().query(model_name, *args)
def get_model(model_name):
"""Get a model class."""
from homeassistant.components.recorder import models
return getattr(models, model_name)
def log_error(e, retry_wait=0, rollback=True,
message="Error during query: %s"):
"""Log about SQLAlchemy errors in a sane manner."""
import sqlalchemy.exc
if not isinstance(e, sqlalchemy.exc.OperationalError):
_LOGGER.exception(e)
else:
_LOGGER.error(message, str(e))
if rollback:
Session().rollback()
if retry_wait:
_LOGGER.info("Retrying failed query in %s seconds", QUERY_RETRY_WAIT)
time.sleep(retry_wait)
class Recorder(threading.Thread):
"""A threaded recorder class."""
# pylint: disable=too-many-instance-attributes
def __init__(self, hass, purge_days, uri):
"""Initialize the recorder."""
threading.Thread.__init__(self)
self.hass = hass
self.purge_days = purge_days
self.queue = queue.Queue()
self.quit_object = object()
self.recording_start = dt_util.utcnow()
self.db_url = uri
self.db_ready = threading.Event()
self.engine = None
self._run = None
def start_recording(event):
"""Start recording."""
self.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_recording)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
hass.bus.listen(MATCH_ALL, self.event_listener)
def run(self):
"""Start processing events to save."""
from homeassistant.components.recorder.models import Events, States
import sqlalchemy.exc
global _INSTANCE
while True:
try:
self._setup_connection()
self._setup_run()
break
except sqlalchemy.exc.SQLAlchemyError as e:
log_error(e, retry_wait=CONNECT_RETRY_WAIT, rollback=False,
message="Error during connection setup: %s")
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()
if event == self.quit_object:
self._close_run()
self._close_connection()
_INSTANCE = None
self.queue.task_done()
return
elif event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done()
continue
session = Session()
dbevent = Events.from_event(event)
session.add(dbevent)
for _ in range(0, RETRIES):
try:
session.commit()
break
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT,
rollback=True)
if event.event_type != EVENT_STATE_CHANGED:
self.queue.task_done()
continue
session = Session()
dbstate = States.from_event(event)
for _ in range(0, RETRIES):
try:
dbstate.event_id = dbevent.event_id
session.add(dbstate)
session.commit()
break
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT,
rollback=True)
self.queue.task_done()
def event_listener(self, event):
"""Listen for new events and put them in the process queue."""
self.queue.put(event)
def shutdown(self, event):
"""Tell the recorder to shut down."""
self.queue.put(self.quit_object)
self.queue.join()
def block_till_done(self):
"""Block till all events processed."""
self.queue.join()
def block_till_db_ready(self):
"""Block until the database session is ready."""
self.db_ready.wait()
def _setup_connection(self):
"""Ensure database is ready to fly."""
# pylint: disable=global-statement
global Session
import homeassistant.components.recorder.models as models
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
if self.db_url == 'sqlite://' or ':memory:' in self.db_url:
from sqlalchemy.pool import StaticPool
self.engine = create_engine(
'sqlite://',
connect_args={'check_same_thread': False},
poolclass=StaticPool)
else:
self.engine = create_engine(self.db_url, echo=False)
models.Base.metadata.create_all(self.engine)
session_factory = sessionmaker(bind=self.engine)
Session = scoped_session(session_factory)
self.db_ready.set()
def _close_connection(self):
"""Close the connection."""
global Session
self.engine.dispose()
self.engine = None
Session = None
def _setup_run(self):
"""Log the start of the current run."""
recorder_runs = get_model('RecorderRuns')
for run in query('RecorderRuns').filter_by(end=None):
run.closed_incorrect = True
run.end = self.recording_start
_LOGGER.warning("Ended unfinished session (id=%s from %s)",
run.run_id, run.start)
Session().add(run)
_LOGGER.warning("Found unfinished sessions")
self._run = recorder_runs(
start=self.recording_start,
created=dt_util.utcnow()
)
session = Session()
session.add(self._run)
session.commit()
def _close_run(self):
"""Save end time for current run."""
self._run.end = dt_util.utcnow()
session = Session()
session.add(self._run)
session.commit()
self._run = None
def _purge_old_data(self):
"""Purge events and states older than purge_days ago."""
from homeassistant.components.recorder.models import Events, States
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 = Session().query(Events).filter(
(Events.created < purge_before)).delete(synchronize_session=False)
_LOGGER.debug("Deleted %s events", deleted_rows)
_LOGGER.info("Purging states created before %s", purge_before)
deleted_rows = Session().query(States).filter(
(States.created < purge_before)).delete(synchronize_session=False)
_LOGGER.debug("Deleted %s states", deleted_rows)
Session().commit()
Session().expire_all()
# Execute sqlite vacuum command to free up space on disk
if self.engine.driver == 'sqlite':
_LOGGER.info("Vacuuming SQLite to free space")
self.engine.execute("VACUUM")
def _verify_instance():
"""Throw error if recorder not initialized."""
if _INSTANCE is None:
raise RuntimeError("Recorder not initialized.")
+162
View File
@@ -0,0 +1,162 @@
"""Models for SQLAlchemy."""
import json
from datetime import datetime
import logging
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer,
String, Text, distinct)
from sqlalchemy.ext.declarative import declarative_base
import homeassistant.util.dt as dt_util
from homeassistant.core import Event, EventOrigin, State
from homeassistant.remote import JSONEncoder
from homeassistant.helpers.entity import split_entity_id
# SQLAlchemy Schema
# pylint: disable=invalid-name
Base = declarative_base()
_LOGGER = logging.getLogger(__name__)
class Events(Base):
# pylint: disable=too-few-public-methods
"""Event history data."""
__tablename__ = 'events'
event_id = Column(Integer, primary_key=True)
event_type = Column(String(32), index=True)
event_data = Column(Text)
origin = Column(String(32))
time_fired = Column(DateTime(timezone=True))
created = Column(DateTime(timezone=True), default=datetime.utcnow)
@staticmethod
def from_event(event):
"""Create an event database object from a native event."""
return Events(event_type=event.event_type,
event_data=json.dumps(event.data, cls=JSONEncoder),
origin=str(event.origin),
time_fired=event.time_fired)
def to_native(self):
"""Convert to a natve HA Event."""
try:
return Event(
self.event_type,
json.loads(self.event_data),
EventOrigin(self.origin),
_process_timestamp(self.time_fired)
)
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting to event: %s", self)
return None
class States(Base):
# pylint: disable=too-few-public-methods
"""State change history."""
__tablename__ = 'states'
state_id = Column(Integer, primary_key=True)
domain = Column(String(64))
entity_id = Column(String(64))
state = Column(String(255))
attributes = Column(Text)
event_id = Column(Integer, ForeignKey('events.event_id'))
last_changed = Column(DateTime(timezone=True), default=datetime.utcnow)
last_updated = Column(DateTime(timezone=True), default=datetime.utcnow)
created = Column(DateTime(timezone=True), default=datetime.utcnow)
__table_args__ = (Index('states__state_changes',
'last_changed', 'last_updated', 'entity_id'),
Index('states__significant_changes',
'domain', 'last_updated', 'entity_id'), )
@staticmethod
def from_event(event):
"""Create object from a state_changed event."""
entity_id = event.data['entity_id']
state = event.data.get('new_state')
dbstate = States(entity_id=entity_id)
# State got deleted
if state is None:
dbstate.state = ''
dbstate.domain = split_entity_id(entity_id)[0]
dbstate.attributes = '{}'
dbstate.last_changed = event.time_fired
dbstate.last_updated = event.time_fired
else:
dbstate.domain = state.domain
dbstate.state = state.state
dbstate.attributes = json.dumps(dict(state.attributes))
dbstate.last_changed = state.last_changed
dbstate.last_updated = state.last_updated
return dbstate
def to_native(self):
"""Convert to an HA state object."""
try:
return State(
self.entity_id, self.state,
json.loads(self.attributes),
_process_timestamp(self.last_changed),
_process_timestamp(self.last_updated)
)
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to state: %s", self)
return None
class RecorderRuns(Base):
# pylint: disable=too-few-public-methods
"""Representation of recorder run."""
__tablename__ = 'recorder_runs'
run_id = Column(Integer, primary_key=True)
start = Column(DateTime(timezone=True), default=datetime.utcnow)
end = Column(DateTime(timezone=True))
closed_incorrect = Column(Boolean, default=False)
created = Column(DateTime(timezone=True), default=datetime.utcnow)
def entity_ids(self, point_in_time=None):
"""Return the entity ids that existed in this run.
Specify point_in_time if you want to know which existed at that point
in time inside the run.
"""
from sqlalchemy.orm.session import Session
session = Session.object_session(self)
assert session is not None, 'RecorderRuns need to be persisted'
query = session.query(distinct(States.entity_id)).filter(
States.last_updated >= self.start)
if point_in_time is not None:
query = query.filter(States.last_updated < point_in_time)
elif self.end is not None:
query = query.filter(States.last_updated < self.end)
return [row[0] for row in query]
def to_native(self):
"""Return self, native format is this model."""
return self
def _process_timestamp(ts):
"""Process a timestamp into datetime object."""
if ts is None:
return None
elif ts.tzinfo is None:
return dt_util.UTC.localize(ts)
else:
return dt_util.as_utc(ts)
+10 -2
View File
@@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
REQUIREMENTS = ['pyRFXtrx==0.8.0']
REQUIREMENTS = ['pyRFXtrx==0.9.0']
DOMAIN = "rfxtrx"
@@ -40,6 +40,7 @@ DATA_TYPES = OrderedDict([
('Rain rate', ''),
('Energy usage', 'W'),
('Total usage', 'W'),
('Sound', ''),
('Sensor Status', ''),
('Unknown', '')])
@@ -65,6 +66,9 @@ def _valid_device(value, device_type):
key = device.get('packetid')
device.pop('packetid')
if not len(key) % 2 == 0:
key = '0' + key
if get_rfx_object(key) is None:
raise vol.Invalid('Rfxtrx device {} is invalid: '
'Invalid device id for {}'.format(key, value))
@@ -159,7 +163,11 @@ def get_rfx_object(packetid):
"""Return the RFXObject with the packetid."""
import RFXtrx as rfxtrxmod
binarypacket = bytearray.fromhex(packetid)
try:
binarypacket = bytearray.fromhex(packetid)
except ValueError:
return None
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
if pkt is None:
return None
@@ -0,0 +1,101 @@
"""
The homematic rollershutter platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/rollershutter.homematic/
Important: For this platform to work the homematic component has to be
properly configured.
"""
import logging
from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN)
from homeassistant.components.rollershutter import RollershutterDevice,\
ATTR_CURRENT_POSITION
import homeassistant.components.homematic as homematic
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematic']
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the platform."""
if discovery_info is None:
return
return homematic.setup_hmdevice_discovery_helper(HMRollershutter,
discovery_info,
add_callback_devices)
class HMRollershutter(homematic.HMDevice, RollershutterDevice):
"""Represents a Homematic Rollershutter in Home Assistant."""
@property
def current_position(self):
"""
Return current position of rollershutter.
None is unknown, 0 is closed, 100 is fully open.
"""
if self.available:
return int((1 - self._hm_get_state()) * 100)
return None
def position(self, **kwargs):
"""Move to a defined position: 0 (closed) and 100 (open)."""
if self.available:
if ATTR_CURRENT_POSITION in kwargs:
position = float(kwargs[ATTR_CURRENT_POSITION])
position = min(100, max(0, position))
level = (100 - position) / 100.0
self._hmdevice.set_level(level, self._channel)
@property
def state(self):
"""Return the state of the rollershutter."""
current = self.current_position
if current is None:
return STATE_UNKNOWN
return STATE_CLOSED if current == 100 else STATE_OPEN
def move_up(self, **kwargs):
"""Move the rollershutter up."""
if self.available:
self._hmdevice.move_up(self._channel)
def move_down(self, **kwargs):
"""Move the rollershutter down."""
if self.available:
self._hmdevice.move_down(self._channel)
def stop(self, **kwargs):
"""Stop the device if in motion."""
if self.available:
self._hmdevice.stop(self._channel)
def _check_hm_to_ha_object(self):
"""Check if possible to use the HM Object as this HA type."""
from pyhomematic.devicetypes.actors import Blind
# Check compatibility from HMDevice
if not super()._check_hm_to_ha_object():
return False
# Check if the homematic device is correct for this HA device
if isinstance(self._hmdevice, Blind):
return True
_LOGGER.critical("This %s can't be use as rollershutter!", self._name)
return False
def _init_data_struct(self):
"""Generate a data dict (self._data) from hm metadata."""
super()._init_data_struct()
# Add state to data dict
self._state = "LEVEL"
self._data.update({self._state: STATE_UNKNOWN})
+4 -23
View File
@@ -7,9 +7,10 @@ https://home-assistant.io/components/rollershutter.wink/
import logging
from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.7']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pywink.get_shades())
class WinkRollershutterDevice(RollershutterDevice):
class WinkRollershutterDevice(WinkDevice, RollershutterDevice):
"""Representation of a Wink rollershutter (shades)."""
def __init__(self, wink):
"""Initialize the rollershutter."""
self.wink = wink
self._battery = None
WinkDevice.__init__(self, wink)
@property
def should_poll(self):
"""Wink Shades don't track their position."""
return False
@property
def unique_id(self):
"""Return the ID of this wink rollershutter."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the rollershutter if any."""
return self.wink.name()
def update(self):
"""Update the state of the rollershutter."""
return self.wink.update_state()
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def move_down(self):
"""Close the shade."""
self.wink.set_state(0)
@@ -0,0 +1,76 @@
"""
Support for Zwave roller shutter components.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/rollershutter.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.rollershutter import DOMAIN
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.rollershutter import RollershutterDevice
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Z-Wave roller shutters."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL:
return
if value.index != 0:
return
value.set_change_verified(False)
add_devices([ZwaveRollershutter(value)])
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
"""Representation of an Zwave roller shutter."""
def __init__(self, value):
"""Initialize the zwave rollershutter."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
self.update_ha_state(True)
_LOGGER.debug("Value changed on network %s", value)
@property
def current_position(self):
"""Return the current position of Zwave roller shutter."""
return self._value.data
def move_up(self, **kwargs):
"""Move the roller shutter up."""
self._node.set_dimmer(self._value.value_id, 100)
def move_down(self, **kwargs):
"""Move the roller shutter down."""
self._node.set_dimmer(self._value.value_id, 0)
def stop(self, **kwargs):
"""Stop the roller shutter."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_BINARY).values():
# Rollershutter will toggle between UP (True), DOWN (False).
# It also stops the shutter if the same value is sent while moving.
value.data = value.data
break

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