Compare commits

...

476 Commits

Author SHA1 Message Date
Robbie Trencheny dfc38b76a4 Merge pull request #3060 from home-assistant/dev
0.27.1
2016-08-30 14:22:01 -07:00
Robbie Trencheny e9354bb1e8 Make pep8 happy 2016-08-30 13:58:53 -07:00
Robbie Trencheny d907902af8 0.27.1 NOT 0.28.1, thanks for the catch @arsaboo 2016-08-30 13:44:35 -07:00
Robbie Trencheny 9a4447ca13 0.28.1 2016-08-30 13:37:47 -07:00
Fabian Affolter eec96ea137 Migrate to voluptuous (#2954) 2016-08-30 21:34:33 +02:00
John Arild Berentsen 7ceb22a08b Ecobee (#3055)
* Added in list for opreation

* Added attribute to reflect the old operation

* fix humidity
2016-08-30 21:04:53 +02:00
Pascal Vizeli cf9b49ac03 update ha-ffmpeg version to 0.9 (#3059) 2016-08-30 20:58:37 +02:00
Johann Kellerman 55d305359e Device tracker component & platform validation. No more home_range. (#2908)
* Device tracker component & platform validation. No more home_range.

* Mock, bluetooth

* Renamed _CONFIG_SCHEMA. Raise warning for #1606

* test duplicates

* Fix assert

* Coverage

* Typing

* T fixes
2016-08-30 10:22:52 -06:00
Daniel Høyer Iversen 16e0187fcc Merge pull request #3051 from tchellomello/added_random_effect_flux_led
Added option to use effect:random for Flux Led light bulbs
2016-08-30 09:52:50 +02:00
Marcelo Moreira de Mello 650ec1a337 Added option to use effect:random for Flux Led light bulbs 2016-08-29 19:55:01 -04:00
Pascal Vizeli 4e044361c3 Use voluptuous for smtp (#3048)
Make note of the breaking change in release notes
2016-08-29 16:56:40 -06:00
Fabian Affolter c1794d111e Upgrade sendgrid to 3.2.10 (#3044) 2016-08-29 14:16:18 -06:00
Fabian Affolter 008e3000bb Upgrade TwitterAPI to 2.4.2 (#3043) 2016-08-29 14:16:10 -06:00
Greg Dowling 1b718c62a3 Fix bug in wemo discovery caused by voluptuous addition. (#3027) 2016-08-29 07:45:48 -06:00
Daniel Høyer Iversen 6275cffab4 Merge pull request #3036 from home-assistant/bug_fix_asuwrt
Bug fix for asuswrt device_tracker. Issue #3015
2016-08-29 08:36:42 +02:00
Daniel 62bbda1f82 Bug fix for asuswrt device_tracker. Issue #3015 2016-08-29 08:23:20 +02:00
arsaboo 39402aff2e Remove units for humidity in Wundeground sensor (#3018)
* Remove units for humidity

Wunderground returns the information with the units.

* Trim the % from the return value of humidity
2016-08-28 21:05:28 -06:00
Martin Hjelmare 1699885907 Fix media_player descriptions and select_source (#3030)
🐬
2016-08-28 20:00:43 -06:00
Teagan Glenn b6ad0bfbea Allow user to configure server id to perform speed test against (#3008)
* Allow user to configure server id to perform speed test against

* Don't overwrite list

* Type-o

* Convert to string

* Append lists

* str(None) => 'None' did not realize that.
2016-08-28 19:09:34 -06:00
Daniel Høyer Iversen 821b3d7fac Bug fix for asuswrt device_tracker. Issue #3015 (#3016)
🐬
2016-08-28 17:34:01 -06:00
John Arild Berentsen 2d8bc754c8 Ecobee (#3025)
* fix ecobee mode

* Fixup
2016-08-28 22:51:56 +02:00
John Arild Berentsen 2a5ca1c873 Map Modes to setpoint indexes (#3023)
* Map Modes to setpoint indexes

* Fixes devices with no thermostat mode

* another try to fix devices without mode

* another try to fix devices without mode 2

* another try to fix devices without mode 3

* fix setting setpoint for devices with no mode

* fix setting setpoint for devices with no mode
2016-08-28 22:41:48 +02:00
Pascal Vizeli 3313995c4c fix voluptuous and cover autodiscovery (#3022) 2016-08-28 20:00:44 +02:00
John Arild Berentsen 17a57d3b47 Fixes wrong statevalue and problem with zwave setpoint (#3017)
* Fixes wrong statevalue and problem with zwave setpoint

* Fix demo test to match bugfix (#10)
2016-08-28 17:58:50 +02:00
Robbie Trencheny 7940648725 0.28.0.dev0 2016-08-27 21:07:55 -07:00
Robbie Trencheny 7b2f0e709b Merge pull request #2952 from home-assistant/dev
0.27
2016-08-27 20:27:52 -07:00
Robbie Trencheny 78675ed3f8 Version bump to 0.27.0 2016-08-27 20:07:20 -07:00
Robbie Trencheny 43555b646c update frontend 2016-08-27 20:05:44 -07:00
Teagan Glenn fdb6de4d23 Fan demo (#2976)
* Update attr to property and default state method

* State prop is defined in parent class

* Demo platform fan

* PyDoc

* Copy-pasta artifact

* PyDoc

* Linting

* Raise error if turn_off and turn_on not implemented

* Update demo platform

* Initial unit test commit

* Readability

* Unneeded typing

* Should inherit from fan entity

* Turn off polling

* Initial oscillating flag

* Pass HASS into demo

* Typing

* Invoke set_speed instead of setting directly

* Service update

* Update demo tests

* Forgot to block after service call.

* linting

* Test to make sure not implemented is thrown

* Is On Method test

* Update const to match string

* Update services yaml

* Toggle method

* Toggle service

* Typing

* TYPE O

* Attribute check

* Type-o

* Type-o

* Put typing back

* ToggleEntity

* Linting

* Linting

* Oops

* Stale prints

* Demo support
2016-08-27 14:53:12 -06:00
Fabian Affolter a4b90c9879 Use voluptuous for Raspberry Pi and local file camera (#2988)
* Migrate to voluptuous

* Update const.py

* Migrate to voluptuous

* Remove duplicate _LOGGER entry
2016-08-27 14:44:22 -06:00
Fabian Affolter 2accc15d41 Migrate to voluptuous (#2991)
🐬
2016-08-27 14:43:33 -06:00
Fabian Affolter e6b9d5f5b3 Migrate to voluptuous (#2989)
🍪 🐬
2016-08-27 14:42:34 -06:00
Martin Hjelmare 6acaf25b0d Use voluptuous for mysensors (#2992)
* Add voluptuous config validation for mysensors
* Remove and clean up parts that are not needed for pymysensors 0.7.
2016-08-27 14:41:21 -06:00
Johann Kellerman 6f1c97b9d3 Voluptuous for AsusWRT (#2998)
* Voluptuous for AsusWRT
2016-08-27 22:30:06 +02:00
John Arild Berentsen f863efdaca Use COMMAND_CLASS_THERMOSTAT_SETPOINT to get unit_of_measurement instad of COMMAND_CLASS_SENSOR_MULTILEVEL. Not all devices have multilevel sensor. (#3003) 2016-08-27 13:39:22 +02:00
Robbie Trencheny 04f0fec352 Merge pull request #2970 from persandstrom/modbus_write
modbus write register service
2016-08-27 03:07:34 -07:00
Per Sandström 2c26514c95 modbus sensor value scaling (#2972) 2016-08-27 02:49:49 -07:00
Matthew Bowen c05d27d214 Completely local control of entities via Alexa (#2942)
* Initial code for alexa_local_control.

* Added support for creating a dummy username.

* Move SSDP responses to local variables.

* Added config validation via Voluptuous.

* Modify and remove unnecessary returned emulated bridge values.

* Remove script and scene domains from default exposed domains.

* Replaced Flask with HomeAssistantWSGI.

* Fix lint errors.

* Correcting grammar and spelling in docs and comments.

* Rename alexa_local_control to emulated_hue.

* Rename emulated_hue attributes.

* Fix a bug where something marked not exposed is exposed by default.

* Make sure the UPNP responder thread cleanly stops when HASS stops.

Also fix some config loading and lint errors.

* Fixed unexposed entities still having individual state exposed.

* Started writing tests for emulated_hue.

* Fix being able to set state of non-exposed entity.

* Another test for emulated_hue.

* More tests for emulated_hue.

Also slightly simplified emulated_hue's PUT handler.

* Fix bad test, sorry :/

* Third time's the charm.

* Fix lint and value validation tests.

* Rename emulated_hue bridge name.

* Remove license and documentation from header.

* Combine two if statements.

* Style changes.

* Fixed various issues and added some constants
2016-08-27 01:23:40 -07:00
icovada a4b8c3cab0 Update telegram.py add send_document (#2937)
🐬
2016-08-26 18:52:44 -06:00
Johann Kellerman 4aad83d60b Voluptuous for pushover (#3000) 2016-08-26 17:43:59 -07:00
Johann Kellerman 37048919bf Check config requirement fix (#2999)
* Check config requirement fix
2016-08-27 01:33:57 +02:00
Fabian Affolter 5cc672ea59 Migrate to voluptuous (#2990)
🐬
2016-08-26 14:50:32 -06:00
Per Sandström ead0559661 Merge pull request #2995 from persandstrom/vsure_10.2
bump vsure version
2016-08-26 21:53:31 +02:00
Per Sandström 4ee37cb8c8 bump vsure version 2016-08-26 21:38:49 +02:00
Pascal Vizeli 9ab2ac766e add motion sensor / rewrite ffmpeg binary sensor (#2969)
🐬 

* use const flags
2016-08-26 06:48:17 -06:00
Pascal Vizeli d2bb61ad9e Change variable to poll for ccu/homegear (#2987) 2016-08-26 12:17:50 +02:00
Pascal Vizeli 95b98f6752 Full homematic system variable support (#2986) 2016-08-26 10:25:56 +02:00
Martin Hjelmare 877dc9c7b5 Merge pull request #2960 from MartinHjelmare/mysensors-mqtt
Add MQTT gateway for MySensors
2016-08-26 08:10:57 +02:00
Roi Dayan d611010a6e Fix reading dht config values (#2956)
🐬
2016-08-25 20:09:48 -06:00
Pascal Vizeli 2eadae2039 add homematic hub device with variable support / update pyhomematic with new device / add cover support (#2981) 2016-08-25 21:55:03 +02:00
Daniel Høyer Iversen 354f4b4740 Upgrade rfxtrx lib (#2974) 2016-08-25 10:52:48 -07:00
MartinHjelmare d1e94b958f Extract mqtt string into constant and add log 2016-08-25 19:07:22 +02:00
Landrash 34f57ebdc9 Fix reference to wrong components in tests for cameras (#1) (#2975)
🐬 👍
2016-08-25 10:55:37 -06:00
Teagan Glenn 826ec9b9d7 Add a Fan component and support for an Insteon Hub Fan (#2964)
* Fan component and service definitions
* Insteon Hub fan support
2016-08-25 14:47:07 +02:00
Per SandstrÃom be1981ca5d modbus write register service 2016-08-25 08:20:08 +00:00
Johann Kellerman 17631cd728 Check config script: various fixes (#2967)
🐬
2016-08-24 23:18:32 -06:00
Fabian Affolter b199c61c88 Migrate to voluptuous (#2955)
🐬
2016-08-24 22:36:41 -06:00
Fabian Affolter 9219d65c3e Migrate to voluptuous (#2958) 2016-08-24 22:35:09 -06:00
John Arild Berentsen d9322b81f3 Bugfixing DemoCover NotImplemented Was not raised for is_closed (#2965)
* NotImplemented was not raised when is_closed was missing

* Bugfixing Demo
2016-08-24 20:36:43 +02:00
Open Home Automation cc358a5dde Corrected sensor name from HM-Z19 to MH-Z19 (#2963)
Approved
2016-08-24 10:54:34 -06:00
John Arild Berentsen 5d2d9af8e3 Add deprecated warning to thermostat and hvac (#2962)
* Add deprecated warning for thermostat and hvac
2016-08-24 16:30:14 +02:00
Daniel Høyer Iversen daa066c036 Merge pull request #2959 from home-assistant/rfxtrx
minor bug in rfxtrx sensor
2016-08-24 14:25:07 +02:00
John Arild Berentsen e5abf6074c Cover (#2957)
* current_position was set, it should be optional

* Update mqtt test to match
2016-08-24 11:53:02 +02:00
Daniel 99796e559e minor bug in rfxtrx sensor 2016-08-24 11:33:57 +02:00
MartinHjelmare e7b206da0c Add MQTT gateway for MySensors
* Use mqtt component to enable a MySensors MQTT gateway.
* Setup the MQTT gateway if mysensors config has mqtt as a value for
	the key	device in the list of gateways.
* Simplify two lines in the mqtt component.
2016-08-24 10:48:55 +02:00
Greg Dowling 4795122463 Add voluptuous to binary template sensor (#2938)
* Add voluptuous to binary template sensor / update failing test.

* Update tests.

* Quick fixes to remove duplicate variables
2016-08-24 01:16:26 -07:00
Nolan Gilley 61ef2683c5 Add volume and seek control to gpmdp (#2953) 2016-08-23 23:32:00 -07:00
Carter 52acb2e6f0 adding pull mode and relay time for you garage door (#2896)
* adding pull mode and relay time

* fixing failing tests

* removed unused vars, removed trailing whitespace

* removed white space

* split line in 2

* removed whitespace and fixed indent

* undid line break

* Update rpi_gpio.py

new line so its not too long

* back to no new line

* Moved long method to a new line

* Moved comment

* moved comment to above method

* adding required blank line

* fixed variables and made them optional

misunderstood the logic at first.

* removed line for lint and removed vars that were not required

* added second blank line for class

* added new configs to platform_schema - still have same error on load

* changing string to int

* added code to covers rpi as well
2016-08-23 23:28:49 -07:00
Robby Grossman 78b2c87b54 Implement support for NEST structures. (#2736)
* Implement support for NEST structures.

* Conform to balloobbot style preferences.

* Log to debug level rather than info level.

* Use config validation to coerce list format if supplied as string.

* Use list comprehension for more succinct code.

* Conform to project linting standards.
2016-08-23 22:47:53 -07:00
Robbie Trencheny 5d4dc713f2 Append the travel mode to the sensor name for Google Travel Time 2016-08-23 21:01:31 -07:00
Paulus Schoutsen 21fb18e5aa pep257 fixes 2016-08-23 20:25:52 -07:00
Paulus Schoutsen c4b53039c1 Merge remote-tracking branch 'origin/master' into dev 2016-08-23 19:39:03 -07:00
Paulus Schoutsen 63e3d20260 update frontend 2016-08-23 19:36:45 -07:00
Nolan Gilley 0c91ba4a50 improve gpmdp (#2951) 2016-08-23 19:09:43 -07:00
Pascal Vizeli c5fd665151 add ffmpeg noise detection sensor (#2950) 2016-08-23 19:08:20 -07:00
Fabian Affolter 98364248d4 Use voluptuous for graphite (#2929)
* Migrate to voluptuous

* Update tests

* Fix tests and check if Graphite instance is reachable
2016-08-23 19:01:46 -07:00
Fabian Affolter 6f27d58188 Use voluptuous for Splunk (#2931)
* Migrate to voluptuous

* Update tests
2016-08-23 18:58:59 -07:00
John Arild Berentsen cf832499cd Combine garage_door and rollershutter to cover (#2891)
* First draft for cover component

* Efficiency from @martinhjelmare

* migrate demo

* migrate demo test

* migrate command_line rollershutter

* migrate command_line test

* migrate rpi_gpio garage_door

* make some abstract methods optional

* migrate homematic

* migrate scsgate

* migrate rfxtrx and test

* migrate zwave

* migrate wink

* migrate mqtt rollershutter and test

* requirements

* coverage

* Update mqtt with garage door

* Naming and cleanup

* update test_demo.py

* update demo and core

* Add deprecated warning to rollershutter and garage_door

* Naming again

* Update

* String constants

* Make sure set_position works properly in demo too

* Make sure position is not set if not available.

* Naming, and is_closed

* Update zwave.py

* requirements

* Update test_rfxtrx.py

* fix mqtt

* requirements

* fix wink version

* Fixed demo test

* naming
2016-08-23 18:23:18 -07:00
Fabian Affolter a43ea81d8e Migrate to voluptuous (#2927) 2016-08-23 17:27:54 -07:00
Paulus Schoutsen 2b4f0cb5a1 Fix broken template sensor tests 2016-08-23 00:14:45 -07:00
Paulus Schoutsen 88573667fa Update frontend 2016-08-23 00:06:58 -07:00
Greg Dowling dfd76fc0e6 Minor tidy of voluptuous. (#2945) 2016-08-22 23:57:07 -07:00
Greg Dowling 5abb46a809 Tidy voluptuous. (#2946) 2016-08-22 23:56:39 -07:00
Robbie Trencheny 82de1cd6fe change const.py to use single quotes 2016-08-22 23:15:22 -07:00
Paulus Schoutsen c9d5d1a417 Remove debug print 2016-08-22 21:44:58 -07:00
Johann Kellerman 14b034f452 Check config script (#2657)
* Add check_config, yaml linting script

* WIP: Start reusing some bootstrap methods for validation

* Start outputs

* Secrets, files and failed config

* requirements_all

* Fixes

* formatting

* Fix unit test after formatting
2016-08-22 21:42:05 -07:00
William Scanlon f00cdc50df Updated python-wink version to fix color/temp detection (#2935) 2016-08-22 21:31:17 -07:00
Johann Kellerman 0def842231 Quick lint script for changed files (#2941) 2016-08-22 20:52:31 -07:00
Greg Dowling dfca2476bd Add voluptuous to efergy. (#2943) 2016-08-22 20:51:17 -07:00
Greg Dowling 9fcfc213c7 Bump pywemo. (#2944) 2016-08-22 20:50:05 -07:00
Greg Dowling eac67fd971 Add voluptuous to template switch (#2940)
* Add voluptuous to template switch / revise tests.
2016-08-23 00:05:45 +02:00
Fabian Affolter e5969f0733 Clean-up (#2933) 2016-08-22 14:20:04 +02:00
Fabian Affolter fb639e08d7 Fix schemas and update ordering (#2932) 2016-08-22 14:19:19 +02:00
Fabian Affolter b6da4a53d5 Use voluptuous for dweet and arduino (#2926)
* Migrate to voluptuous

* Migrate to voluptuous

* One import is enough
2016-08-22 11:28:58 +02:00
Greg Dowling 32318c6f19 Add voluptuous validation to template sensor. (#2886) 2016-08-22 01:11:16 -07:00
Fabian Affolter 5d816b5eb5 Use voluptuous for OhmConnect (#2906)
* Migrate to voluptuous

* Remove string
2016-08-22 08:20:31 +02:00
Paulus Schoutsen 0d7d125344 Update frontend 2016-08-21 16:58:42 -07:00
Paulus Schoutsen 7598de90cb Allow unregistering a push subscription (#2921)
* Allow unregistering a push subscription

* Update frontend

* ps - HTML5 tests DRY 🍾
2016-08-21 16:01:24 -07:00
Teagan Glenn d2f7b3c7db Merge pull request #2922 from dpford/tplink-5g
Add tplink Archer C7 device tracking support for 5Ghz networks
2016-08-21 16:49:19 -06:00
Jesse Newland 520d4d5dc0 Add zwave.rename_node service (#2923)
* Add zwave.rename_node service

* Validate service data

* Better schema
2016-08-21 14:36:44 -07:00
Dan Ford 2b4980ae5d Add tplink Archer C7 device tracking support for 5Ghz networks 2016-08-21 13:09:44 -07:00
Josh Nichols d70d1e1303 Add support for notifying with Slack attachments. (#2914)
* Add support for notifying with Slack messages.

When creating notifications, this allows you to pass in `attachments`
with the `data`. It's an array of attachments as defined in
https://api.slack.com/docs/message-attachments

When passing in attachments, message is still required, but it's okay to
be a blank string.

* Split over multiple lines

* Make sure attachments gets assigned, even if there isn't attachment data
2016-08-21 11:54:28 -07:00
Paulus Schoutsen f802d6bfa3 Update test packages (#2918) 2016-08-21 11:44:40 -07:00
Nolan Gilley 635e5c8eba Add voluptuous to ecobee, speedtest.net, fast.com, actiontec, forecast.io (#2872)
* add voluptuous

* fixes for comments

* str to cv.string
2016-08-21 10:29:13 -07:00
Martin Hjelmare fa3d83118a Merge pull request #2917 from hensing/update_mysensors
Update pymysensors version to 0.7.1
2016-08-21 13:16:49 +02:00
Henning Dickten a12dadab5e Update pymysensors version to 0.7.1 2016-08-21 12:47:40 +02:00
Paulus Schoutsen 23e86fc8ea Update frontend 2016-08-20 23:44:31 -07:00
Paulus Schoutsen aa6a0523ef Add template support to generic camera + local file tests (#2881)
* Add template support to generic camera

* Add tests for local file
2016-08-20 23:04:55 -07:00
Teagan Glenn 9cfad34866 Merge pull request #2916 from Teagan42/YamlSecret-LoadBeforeBreak
Approved by @balloob via gitter
2016-08-20 22:20:11 -06:00
Teagan M. Glenn af22aeeba8 Apparently, doesn't load the root config secret 2016-08-20 22:07:21 -06:00
Heiko Rothe 6aa0789e38 MQTT room presence detection (#2913)
* Added room presence tracker

* Fixed room/device discovery bugs

* Added tests for room tracker

* Fixed some formatting mistakes

* Fixed a tiny bug with the track new option

* Converted device tracker into sensor

* Removed leftover service entry

* Changed name to mqtt_room

* Changed payload validation to voluptuous

* Fixed validation

* Removed sleep from tests
2016-08-20 20:49:38 -07:00
Open Home Automation 46dcfb3d70 Serial CO2 sensor support (#2885)
* Added support for serial HM-Z19 CO2 sensor

* Minor pylint bug fixes

* Added new files to .coveragerc

* Removed newline

* Changes in requirements after change of pmsensor library

* Change the implementation of default name

* Check if serial interface is working before adding the sensor

* Maximum sensor value is 5000ppm
2016-08-20 16:35:10 -07:00
Fabian Affolter 5f508b6afa Use voluptuous for REST platforms (#2887)
* Initial step to migrate to voluptuous

* Migrate to voluptuous

* Add schema for sensor_classes
2016-08-20 16:28:45 -07:00
Roi Dayan b62c3ac56c Update dht sensor dependency Adafruit_DHT to v1.3.0 (#2900)
The repository already merged the pull request adding python3 support.
root is no longer required to use the gpio.

Signed-off-by: Roi Dayan <roi.dayan@gmail.com>
2016-08-20 15:41:58 -07:00
Fabian Affolter 18fd17fdf3 Migrate to voluptuous (#2901) 2016-08-20 15:41:14 -07:00
Fabian Affolter e8c6e4d561 Clean-up, ordering, constants, and extend of schema (#2903)
* Clean-up, ordering, constants, and extend of schema

* Put REQUIREMENTS back and re-add line breaks

* Clean-up, ordering, constants, and extend of schema

* Extend platform
2016-08-20 15:40:16 -07:00
Fabian Affolter 8fc27cbe43 Migrate to voluptuous (#2905) 2016-08-20 15:28:26 -07:00
Fabian Affolter 502c65ca32 Migrate to voluptuous (#2907) 2016-08-20 15:25:11 -07:00
Paulus Schoutsen 3fae4fefbf Bust cache for new media player covers (#2882) 2016-08-20 15:14:57 -07:00
Daniel Perna 2558501235 Added LlamaLab Automate notify platform (#2863)
* Addod LlamaLab Automate notify platform

* Added platform to .coveragerc

* Added device-option and switched to voluptuous

* Fixed voluptuous usage
2016-08-20 15:11:37 -07:00
Martin Hjelmare b7eee6fbb3 Merge pull request #2910 from hensing/update_mysensors
Update pymysensors version to 0.7
2016-08-20 23:32:17 +02:00
Henning Dickten 7d0c50a106 Update pymysensors version to 0.7 2016-08-20 22:45:55 +02:00
Teagan Glenn 8d1a9d86ea Yaml secret fallback to parent folders (#2878)
* Move secret cache out of loader so it can be referenced by other folders
* Unit test to verify secrets from another folder work & see if it overrides parent secret
* Clear secret cache after load
2016-08-20 21:39:56 +02:00
Per Sandström ca75e66c1a Merge pull request #2902 from persandstrom/vsure_10.1
vsure 10.1
2016-08-20 17:23:07 +02:00
Per Sandström b5cc145a92 bump vsure version 2016-08-20 17:08:42 +02:00
Greg Dowling a46230b830 Merge pull request #2892 from home-assistant/vera_voluptuous
Add voluptuous to Vera.
2016-08-20 16:05:51 +01:00
pavoni c0cd2d749f Tidy. 2016-08-20 15:43:07 +01:00
Greg Dowling 2df85242f9 Merge pull request #2890 from home-assistant/loop_energy_voluptuous
Add voluptuous to Loopenergy
2016-08-20 15:34:53 +01:00
Josh Nichols 8eb66ac2b8 Ensure Slack messages appear as correct user (#2893)
Current documentation suggests to use personal API tokens. This isn't
ideal because for a few reasons:

* messages will come as your own user, so it's hard to tell it's coming
  from hass
* it's harder to manage if multiple people are using Slack and home
* assistant, since you'd have to coordinate rolling of it

It is possible to use Slack bot users already. Just make a new one from https://your-team.slack.com/apps/build/custom-integration, and use the token for that. You can even add an icon from the web frontend for home assistant.

However, the message will appear as a bot without a name or icon. This pull requests fixes this by passing the as_user parameter, which uses the bot user's name and icon.

One caveat is you need to invite the bot user into the room you want to
post to. This probably was an issue before though.

🎩 to @jnewland who pointed me to this in his branch
2016-08-20 14:36:28 +02:00
Greg Dowling 482f32bb87 Add voluptuous to wemo. (#2895) 2016-08-20 14:03:57 +02:00
John Arild Berentsen 6e672b7bee More generic use of up and down commands plus a workaround for SOMFY controller. (#2852)
* Support older devices

* Update with workaround

* Inverted up and down

* Make dual arguments
2016-08-20 13:59:23 +02:00
Pascal Vizeli a50463d2f1 Improve homematic climate support (#2894) 2016-08-20 00:20:41 +02:00
pavoni 712f1498ae Add voluptuous to Vera. 2016-08-19 22:43:40 +01:00
pavoni d1a31b3e0c Change CONFIG to CONF for consistency. 2016-08-19 21:47:07 +01:00
pavoni 337b2e3f77 Add voluptuous
.# Please enter the commit message for your changes. Lines starting
2016-08-19 20:07:09 +01:00
Fabian Affolter 4f1712c933 Use voluptuous for system monitoring sensors (#2813)
* Use voluptuous for system monitoring sensors

* Extent platform, ordering, and consts

* Add resource/resources
2016-08-19 14:57:14 +02:00
Open Home Automation def9bbf827 Upgrade pmsensor to 0.3 (#2883) 2016-08-19 14:56:10 +02:00
Fabian Affolter ca1de9cac1 Add url to validation (#2874)
* Add url to validation

* Fix pylint issue

* Clean-up
2016-08-19 13:41:01 +02:00
Fabian Affolter c74e167a7b Use voluptuous for dweet, transmission, and twitch sensor (#2802)
* Use voluptuous for dweet, transmission, and twitch sensor

* Extent platform, ordering, and consts

* Clean-up
2016-08-19 00:18:45 -07:00
John Arild Berentsen ada4de3ffb Migrate Thermostat and HVAC component to climate component (#2825)
* First draft for climate

* Updates for thermostats
2016-08-19 00:17:28 -07:00
Fabian Affolter 0abc50e844 Use voluptuous for transport sensors (#2867) 2016-08-19 00:12:56 -07:00
Nolan Gilley 2a563e1604 binary occupancy sensor (#2869) 2016-08-19 00:11:56 -07:00
Paulus Schoutsen a031d64a44 Merge pull request #2880 from home-assistant/hotfix-026-3
Hotfix 026 3
2016-08-18 22:48:02 -07:00
Paulus Schoutsen a548eb6c7f Version bump to 0.26.3 2016-08-18 22:46:34 -07:00
Paulus Schoutsen 2b10a1ac20 Fix media player art (#2879) 2016-08-18 22:46:17 -07:00
Paulus Schoutsen bafc9413a3 Fix media player art (#2879) 2016-08-18 22:45:42 -07:00
Nolan Gilley 9bfac590f6 fix 2862 (#2868) 2016-08-18 16:21:01 -07:00
Teagan Glenn 85d632c272 Merge pull request #2870 from Teagan42/AddFixToWunderground
Fix PyDoc and other issues with Wunderground - approved by robbie
2016-08-18 17:20:15 -06:00
Teagan Glenn 297fca9351 Type-o 2016-08-18 16:39:16 -06:00
Teagan Glenn cb3a37691f Type-o 2016-08-18 16:28:19 -06:00
Pascal Vizeli df4a9ea1da add move_postion support for HA rollershutter / CONFIG_SCHEMA (#2873) 2016-08-18 23:14:14 +02:00
Teagan M. Glenn 5bdcf60a21 Extend platform schema 2016-08-18 10:47:52 -06:00
Teagan M. Glenn 90449a90f1 Use string templating 2016-08-18 10:46:24 -06:00
Teagan M. Glenn 25840f97c2 Consistent use of WUnderground 2016-08-18 10:46:04 -06:00
Teagan M. Glenn c2b75140bf Fix config validation import to make things more readable 2016-08-18 10:40:28 -06:00
Teagan M. Glenn ec5e20f0d9 Use string constant 2016-08-18 10:38:34 -06:00
Teagan M. Glenn db2d9ec854 Unused property 2016-08-18 10:37:39 -06:00
Teagan M. Glenn ddec28da4b Use schema validators already avaialble 2016-08-18 10:37:26 -06:00
Teagan M. Glenn 6f57d36134 Add doc link to header of file 2016-08-18 10:37:00 -06:00
Teagan M. Glenn 0490fe832a Unneeded validation removed 2016-08-18 10:32:19 -06:00
Teagan M. Glenn 2b8e2a3d36 Remove print lines 2016-08-18 10:27:53 -06:00
Teagan M. Glenn 41f84d9e20 Pydoc for unit test methods 2016-08-18 10:27:38 -06:00
Teagan Glenn c1653d2fca Merge pull request #2861 from arsaboo/patch-2
Wunderground weather sensor
2016-08-18 09:40:00 -06:00
arsaboo 230dde4b57 Removed blank line (linting error) 2016-08-18 10:12:56 -04:00
arsaboo 90fdc89838 Updated to address @balloob's comments 2016-08-18 09:59:41 -04:00
Nolan Gilley 09d531b3b9 fix gpmdp (#2864)
* fix gpmdp

* fix balloobs comments

* move create_connection
2016-08-18 00:08:58 -07:00
Emil Horpen Hetty 053a55bc5f Added name support for Forecast.io (#2638)
* Added support for name

Added name support and changed default name to "Forecast.io" since "Weather" had conflict with Yahoo weather and Open weather map

* Update forecast.py
2016-08-17 23:54:08 -07:00
Open Home Automation ccd8f51253 Ble tracker (#2810)
* Added Bluetooth Low Energy device tracker

* Added new file(s)

* Fixed pylint errors

* Remove traling zeros from device names

* recreated deleted file

* Added requirements

* Renamed to bluetooth_le tracker
Removed gattlib from tests
Minor code cleanup

* - fixed .coveragerc bug
- changed discovery algorithm, new devices will only be added if seen 5 times to make sure
  HA doesn't blow the database with devices just passing by
2016-08-17 23:41:05 -07:00
Roi Dayan 98f236c754 Add webos customize option to add custom sources (#2561)
Currently there are only hw inputs in the sources list.
Other interesting inputs can be live tv (dvbt) and vod apps.

* add customize option for webos
* add short names for livetv, youtube, mako apps
* add current app as a source
* use large icon (largeIcon is the same as icon if doesn't exists)
* filter out hw inputs that are not connected

Signed-off-by: Roi Dayan <roi.dayan@gmail.com>
2016-08-17 23:39:37 -07:00
Robbie Trencheny a5f144cb7c HTML5 notify actions (#2855)
* Add action and callback support to html5 (#2855).

Remove registrations from the callback view since we always get the latest anyway.

We dont put an audience in the claims so we will never hit this error.

Bring tests back up to where they were before callbacks.

Only import jwt where necessary

Fix bracket spacing errors

Fix JWT decode check for loop

Remove stale comment.

Add tests for the callback system.

Shorten line

Disable pylint broad-except and change e to jwt_decode_error.

Verify expiration

Remove duplicate jwt.exceptions.DecodeError

Catch no keys matched and return False

* Switch to using registrations for callbackview instead of json_path

* Only check for URL and such if the data object actually exists

* raise instead of return

* cleanup decode_jwt

* Clean up JWT errors

* Correctly set status_code to 401

* Improve JWT by adding target to claims and attempting to check the given target for a decode match first, as well as pass the target through in the event payload.

* Add tag support and fix formatting issues

* Pass through any keys that dont apply to the payload into the notification.data dictionary

* Remove stale print

* Pass back the data dictionary if it exists

* Actually put the default url even if a notify payload dictionary doesnt exist

* pylint, flake8

* Add subscription validation

* Add validation for the callback event payload and use constants where possible

* Use HTTP_UNAUTHORIZED instead of 401

* Change callback dictionary to dict instead of cv.match_all

* Fix up tests and make subscription required

* Whoops, that test was supposed to fail

* Use the result of CALLBACK_EVENT_PAYLOAD_SCHEMA as event_payload

* Add a test for html5 callback decode_jwt where the device has been renamed since notification has been sent.

* Remove the loop through logic, assume that target is always in JWT

* Always return something instead of possibly None.

* Update frontend
2016-08-17 22:34:12 -07:00
Teagan M. Glenn a5fd04f215 Unit tests around wunderground 2016-08-17 22:33:39 -06:00
Teagan M. Glenn 4e586c18ff Check for error and pull obvservation 2016-08-17 22:32:42 -06:00
Teagan M. Glenn 87f81bf3b4 Use url builder helper 2016-08-17 22:32:19 -06:00
Teagan M. Glenn d2ba8ee0a7 Reraise exception 2016-08-17 22:31:58 -06:00
Teagan M. Glenn 466dd35f3d Don't set state on update - state already handles this 2016-08-17 22:31:47 -06:00
Teagan M. Glenn e54ba5ff72 No need no need to set variable 2016-08-17 22:31:28 -06:00
Teagan M. Glenn dd14f90afb Error handling on state 2016-08-17 22:30:23 -06:00
Teagan M. Glenn ecb4eb843b Don't call update on init of sensor 2016-08-17 22:30:03 -06:00
Teagan M. Glenn afef255a25 Condition is already a string 2016-08-17 22:29:49 -06:00
Teagan M. Glenn 417711d665 Refactoring 2016-08-17 22:29:37 -06:00
Teagan M. Glenn 31237a891c Catch exception from update on initial platform setup 2016-08-17 22:29:25 -06:00
Teagan M. Glenn 62b00e1294 Update invocation of WUndergroundData 2016-08-17 22:29:00 -06:00
Teagan M. Glenn 563154c3c2 Validate configuration 2016-08-17 22:28:18 -06:00
Teagan M. Glenn 1a8e17ce41 Pass hass to constructor 2016-08-17 22:28:05 -06:00
Teagan M. Glenn 42caa31067 Unused variable 2016-08-17 22:22:29 -06:00
Teagan M. Glenn e4abecd359 Build url helper method 2016-08-17 22:22:11 -06:00
Teagan M. Glenn 53b97feb3c Rename constant - make valid for lat/long too 2016-08-17 22:20:22 -06:00
Teagan M. Glenn a09baf1d5a Not using payload 2016-08-17 22:19:57 -06:00
Teagan M. Glenn b7809675eb Config schema 2016-08-17 22:19:13 -06:00
Teagan M. Glenn 333e3ba822 Add imports 2016-08-17 22:18:37 -06:00
David Straub 49998272db Added daily temp/precip forecast values to forecast.io (#2846) 2016-08-17 19:48:51 -07:00
Paulus Schoutsen 8088322c43 Consider core running while starting (#2858) 2016-08-17 18:58:00 -07:00
Johann Kellerman 244f60d6cd Fix script help (#2860)
Allow `--help` to filter down to the script
2016-08-17 18:57:52 -07:00
arsaboo a0bcd33b71 Update wunderground.py 2016-08-17 17:48:37 -04:00
arsaboo be57cd55c5 Update wunderground.py 2016-08-17 17:25:42 -04:00
arsaboo 4dff42e8bb Update wunderground.py 2016-08-17 17:04:11 -04:00
arsaboo 75cd1f8063 Update wunderground.py 2016-08-17 16:50:32 -04:00
arsaboo fae9267701 Update wunderground.py 2016-08-17 16:41:22 -04:00
arsaboo 1a34bc5301 Removed lynting issues 2016-08-17 16:31:36 -04:00
arsaboo aabeda2b60 Update wunderground.py 2016-08-17 16:15:07 -04:00
arsaboo 469d095827 Create initial Wunderground weather sensor 2016-08-17 15:06:12 -04:00
Matthias Grawinkel 8a3c511a04 Adding Digest Auth for webcam image retrieval (#2821)
* Adding Digest Auth for webcam image retrieval

* Update generic.py

* Update mjpeg.py

* Update generic.py

* Update mjpeg.py

* Update generic.py

* Update mjpeg.py
2016-08-17 19:08:47 +02:00
Paulus Schoutsen 2237189c86 Add default badge to push notification (#2859) 2016-08-16 23:05:00 -07:00
Paulus Schoutsen 7720a17c18 Add badge 2016-08-16 23:02:19 -07:00
Pascal Vizeli 4a847dbd91 new yahooweather version and fix update function (#2848) 2016-08-16 22:40:20 -07:00
Robbie Trencheny 848781fbb7 Add a group notify platform (#2842)
* Add a group notify platform which allows sending a single notification to multiple platforms.

* Correctly sort group.py

* Clean up the payload logic

* Make name and entity id required in the schema

* Deep update the dictionary to fix a bug where data wasnt merging.

* Add notify.group tests.

* Improve docstrings.

* Change entities to services and entity_id to service

* Make service a slug without a default value

* Update tests for entities->services, entity_id->service

* vol.Any(cv.slug) -> cv.slug
2016-08-16 22:14:04 -07:00
Robbie Trencheny c1ce6855c5 Expose notify platform targets as individual services (#2837)
* First pass on providing individual services for all possible targets that a notification platform supports.

* Add a quite hacky first version of notification groups

* Add a docstring for get_targets

* Register group service under notifygroup/ and safely check for notifygroups in config

* Remove notifygroups, because it belongs in its own PR

* Make @balloob requested changes

* get_targets()->targets

* Add tests for notify targets exposed as individual services

* If we dont have a platform name set in configuration, lets use the name of the platform instead of notify

* Fix test docstring.

* Dont use a dictionary for just one value

* No need to double slugify

* targets is now just a list of strings instead of a dict
2016-08-16 22:05:41 -07:00
Robbie Trencheny 37561765ff Add Gravatar support to device_tracker (#2836)
* Support passing an email address linked to Gravatar as the picture in known_devices.

* Add a dedicated field for Gravatar

* Bring tests back up to where they were before Gravatar.

* Add tests for Gravatar.
2016-08-16 21:08:57 -07:00
Pascal Vizeli 4fcfffc172 add tcp/udp port to config validation (#2854) 2016-08-16 20:55:29 -07:00
Kevin Gottsman 781fe9c54e Fix logging message for disarm (#2857) 2016-08-16 20:53:59 -07:00
Nolan Gilley 324ddfdaeb fix flux_update service (#2792)
* fix flux_update service

* fix tests

* give update service unique name

* remove unnecessary param

* Revert "fix tests"

This reverts commit 2fd7760455.

* fix flux_update
2016-08-16 20:53:00 -07:00
Maggi Trymbill f668a88485 Fixed typo (#2856)
Pretty sure this is a typo ... it made finding the plist and logs a bit of a headache for me at least :)
2016-08-16 18:24:37 -07:00
Robbie Trencheny 72fc526ee8 Html5 notifications improvements (#2840)
* Retry sending the push for 1 day instead of failing instantly if the target is unavailable

* Add timestamp to push payload

* Correctly use the title and body fields for their intended purposes

* Add callback support

* Revert changes to frontend files.

* Add default URL which will open Home Assistant. Also put all the data into the data object of the payload so it is accessible in the browser. Without doing this, things like URL wouldnt be accessible.

* Flake8 and pylint fixes

* event->type

* Dont send the default url if actions exist

* flake8/pylint fixes again

* Update html5 tests

* Remove callbacks from this branch, will re-stage on a different branch

* Remove remnant of callbacks

* Add url to data dictionary if it exists instead of copying the entire data dictionary in

* flake8 fix
2016-08-16 14:26:01 -07:00
Fabian Affolter 822b7f8770 Use voluptuous for exchange sensors (#2801)
* Use voluptuous for exchange sensors

* Remove additional checks
2016-08-16 22:22:55 +02:00
Fabian Affolter dab5a78f88 Use voluptuous for time/date sensors (#2799)
* Use voluptuous for time/date sensors

* Extend platform

* Remove additional checks
2016-08-16 21:43:56 +02:00
Fabian Affolter 1c140de0dc Use voluptuous for NZB sensors (#2847) 2016-08-16 21:42:43 +02:00
Greg Dowling 91e24de3d5 Merge pull request #2833 from home-assistant/fix_owntracks_beacon_accuracy_bug3
Handle accuracy zero correctly in enter/leave events.
2016-08-16 10:34:27 +01:00
pavoni 41dad9a8f7 Tidy warnings. 2016-08-16 09:48:13 +01:00
Nuno Sousa 7762365b3f Add position to zwave rollershutter (#2772) 2016-08-16 00:37:58 -07:00
Open Home Automation 693098ff00 Bugfix in pknx library (#2835)
* Bugfix in pknx library

* Version pinned to 0.3.3
2016-08-15 23:42:45 -07:00
David Straub 83a043a0ea Add FritzBox call monitor sensor (#2791)
* Add FritzBox call monitor sensor

* Correct docstrings and suppress too few public methods warning

* Remove blank lines after docstrings

* Add blank lines after class docstrings

* Remove trailing white space

* Make daemon; add reconnect on disconnect
2016-08-15 23:22:54 -07:00
Juggels a7f218f712 HP ILO component (#2844)
* HP ILO component

* HP ILO component

* Add Onboard Administrator sensor

* Add Onboard Administrator sensor

* Add period to first line

Fix D400 error on line 1
2016-08-15 23:19:11 -07:00
Assaf Inbal 72ad1d8d7c Added support for exposing light features (#2828) 2016-08-15 23:07:07 -07:00
Nolan Gilley d281a7260d check for runtime error during db query (#2834) 2016-08-15 22:48:42 -07:00
John Arild Berentsen 27e27ee156 Exit when command_classes are missing thermostat Zwave (#2824) 2016-08-15 22:13:49 -07:00
Martin Hjelmare 9afb1d8c0d Fix unit log message (#2823)
* Fix log message for deprecated temp key

* Use string formatting and pass constant variables as arguments in log
	message to show correct name of config keys.

* Fix import order
2016-08-15 22:12:43 -07:00
Paulus Schoutsen 7594cf3c94 Merge pull request #2734 from tobiebooth/manual-alarm-improvements
Return to previous alarm state after trigger (#2580)
2016-08-15 22:10:32 -07:00
pavoni a7703f27d8 Add missed docstring. 2016-08-15 13:14:07 +01:00
pavoni c0b1ff0eaf Handle accuracy zero correctly in enter/leave events. 2016-08-15 13:08:30 +01:00
Paulus Schoutsen f61f0623f8 Merge pull request #2827 from home-assistant/hotfix-0-26-2
Hotfix 0 26 2
2016-08-14 21:25:11 -07:00
Paulus Schoutsen c4b714a10d Version bump to 0.26.2 2016-08-14 21:22:28 -07:00
Greg Dowling 8a8551132f Bump to pywemo 0.4.5 - fixes bug with requests 2.11.0 (#2818) 2016-08-14 21:22:06 -07:00
Greg Dowling 6fd0fe05f9 Bump to pywemo 0.4.5 - fixes bug with requests 2.11.0 (#2818) 2016-08-14 11:42:43 -07:00
Paulus Schoutsen a6bbd749e4 Merge pull request #2817 from home-assistant/hotfix-0-26-1
Hotfix 0 26 1
2016-08-14 01:26:19 -07:00
Paulus Schoutsen 32051c042c Version bump to 0.26.1 2016-08-14 01:21:57 -07:00
John Arild Berentsen c16a29b930 Fix unknown unit of measurement for hvac and thermostat component (#2816)
* Fix unknown unit of measurement for hvac and thermostat component

* Simplify
2016-08-14 01:21:36 -07:00
Heiko Rothe 12ce3deffc Check for existence of system mode on Honeywell thermostats (#2815)
* Check for existence of system mode on Honeywell thermostats

* Return None instead of undefined

* Use getattr instead of if/else
2016-08-14 01:21:28 -07:00
Open Home Automation 7c041f0797 Bugfix: removed conf_platform (#2811)
* Bugfix: removed conf_platform

* Remove unused import

* Fix for wrong update
2016-08-14 01:21:18 -07:00
Heiko Rothe 8210d65850 Check for existence of system mode on Honeywell thermostats (#2815)
* Check for existence of system mode on Honeywell thermostats

* Return None instead of undefined

* Use getattr instead of if/else
2016-08-14 01:20:28 -07:00
John Arild Berentsen bb14239d91 Fix unknown unit of measurement for hvac and thermostat component (#2816)
* Fix unknown unit of measurement for hvac and thermostat component

* Simplify
2016-08-14 01:19:54 -07:00
Paulus Schoutsen dc68f61261 Html5 push notifications notify platform (#2807)
* Initial work to add Chrome Push Notification support

* Remove push.js from home-assistant since it is now in Polymer

* Chrome->HTML5, general cleanup/fixes

* Make html5 generic, move manifest.json into frontend so that we can dynamically add the gcm_sender_id

* Pylint, flake8, pydocstyle frontend init

* HTML5 push fixes

* Update polymer

* Remove crypto req

* Add notify default platform.

* Fix HTML5 push

* Registration fixes

* Linting fix

* pep257 fix

* Add tests

* pep257 fix

* Update frontend
2016-08-14 01:10:07 -07:00
Open Home Automation c6f67a5203 Implemented range checking for temperature and humidity. Out-of-range… (#2805)
* Implemented range checking for temperature and humidity. Out-of-range values will be ignored

* Removed unused import

* Use celsius_to_fahrenheit conversion method
2016-08-14 01:02:26 -07:00
Open Home Automation 8329472c72 Bugfix: removed conf_platform (#2811)
* Bugfix: removed conf_platform

* Remove unused import

* Fix for wrong update
2016-08-13 14:16:06 -07:00
Paulus Schoutsen 8ba85effd4 Version bump to 0.27.0.dev0 2016-08-13 12:02:13 -07:00
Paulus Schoutsen 0270ae05e9 Merge pull request #2760 from home-assistant/dev
0.26
2016-08-13 12:01:56 -07:00
Paulus Schoutsen 9c0b9b9ad6 Version bump to 0.26.0 2016-08-13 12:01:34 -07:00
Robbie Trencheny 7882ce1afd Add CORS fixes to support OPTIONS preflight requests. (#2773)
* Add CORS fixes to support OPTIONS preflight requests.

* Add CORS tests

* Fix formatting
2016-08-13 11:49:44 -07:00
Tobie Booth abaffc2d8c add disarm_after_trigger to manual alarm panel 2016-08-13 12:53:32 -05:00
Tobie Booth 1e3f7ad9a4 Return to previous alarm state after trigger (#2580) 2016-08-13 12:53:32 -05:00
Paulus Schoutsen 176a078b3c Update .coveragerc 2016-08-13 10:39:13 -07:00
Fabian Affolter 5baed6acfb Add support for GPSD (#2254)
* Add support for GPSD

* Add gpsd.py

* Check if socket is open

* Fix pylint issue

* Rename file to be a sensor

* Update for being a sensor

* Rework for being a sensor
2016-08-13 10:37:12 -07:00
Paulus Schoutsen f845893f8f Update frontend 2016-08-13 10:21:54 -07:00
Tomi Tuhkanen 9c636ab6fd Fix for braviatv get mac regex none case (#2808)
* Fix for braviatv get mac regex none case

* E128 fix
2016-08-13 09:45:49 -07:00
Brent Hughes 0df229773f Removed error log on roku connection error (#2809) 2016-08-13 09:45:09 -07:00
Paulus Schoutsen 0b404cc0be Update frontend 2016-08-13 09:41:23 -07:00
Paulus Schoutsen 18829daa65 Merge remote-tracking branch 'origin/master' into dev
Conflicts:
	homeassistant/components/recorder/__init__.py
	homeassistant/const.py
	requirements_all.txt
	setup.py
2016-08-12 18:57:15 -07:00
Pascal Vizeli f0a138dd51 update yahooweather version (#2796) 2016-08-12 18:47:45 -07:00
Daniel Høyer Iversen b28114fb5a Merge pull request #2804 from home-assistant/rfxtrx_log
improve logging from rfxtrx component
2016-08-12 20:56:19 +02:00
Daniel 6d83ebc5e4 improve logging from rfxtrx component 2016-08-12 20:46:54 +02:00
Daniel Høyer Iversen 29bd9b4587 Merge pull request #2803 from home-assistant/rfxtrx_log
improve logging from rfxtrx component
2016-08-12 19:35:44 +02:00
Daniel 5ed22f3ef0 improve logging from rfxtrx component 2016-08-12 19:21:12 +02:00
Daniel Høyer Iversen a14995ed27 Merge pull request #2798 from home-assistant/flux_led_minor_bug
Fix minor bug in flux led
2016-08-12 15:41:22 +02:00
Daniel 0a78b69ee2 Fix minor bug in flux led 2016-08-12 15:21:51 +02:00
Daniel Høyer Iversen b7ebf3b1eb Merge pull request #2789 from home-assistant/rfxtrx_lib
update rfxtrx lib
2016-08-11 14:34:50 +02:00
Daniel 2493155f2b update rfxtrx lib 2016-08-11 14:18:23 +02:00
Fabian Affolter e06ff95107 Remove pylint disable (#2785) 2016-08-11 12:00:37 +02:00
Daniel Høyer Iversen eea7824a7e Merge pull request #2786 from home-assistant/rfxtrx_lib
update rfxtrx lib
2016-08-11 11:41:49 +02:00
Daniel a3c2db70e2 update rfxtrx lib 2016-08-11 11:25:50 +02:00
Fabian Affolter a784f48022 Minor changes (#2784)
* Update link to docs

* Use fast.com

* Update docstring

* Add link to docs

* Add link to docs

* Update docstrings

* Update docstrings

* Fix typo
2016-08-11 11:14:24 +02:00
Paulus Schoutsen e926426af9 Recorder: Increase size of the entity column in states table (#2778)
Fixes https://github.com/home-assistant/home-assistant/issues/2697
2016-08-10 17:40:52 -07:00
Johann Kellerman bf21d6b4e1 Update unit tests for remote.py (#2782)
* Update remote unit tests

* Sleep again
2016-08-10 17:40:35 -07:00
Nolan Gilley dcf4fc5e9b fast.com speedtest sensor (#2783)
* fast.com speedtest sensor

* update for fastdotcom
2016-08-10 17:39:52 -07:00
Johann Kellerman f3376ba276 Script requirement logging, db_migrator REQUIREMENTS (#2781) 2016-08-10 13:32:07 -07:00
Adam Mills 1a327d682d Fix farcy failure for logbook test (#2780) 2016-08-10 08:07:50 -07:00
Corban Mailloux 9c851790dc Add support for new mqtt_json light platform. (#2777)
* Add support for new mqtt_json light platform.

* Fix W503 errors.

* Bring in feedback from @balloob.

* Add test coverage for invalid color and brightness data.

* Add coverage for transition in turn_off.
2016-08-09 23:55:10 -07:00
Johann Kellerman aadf6a7750 Handle requirements for scripts (#2765) 2016-08-09 23:54:34 -07:00
John Arild Berentsen a03691455b Various fixes for missing components and rollershutter. (#2698)
* Various fixes for missing components, rollershutter.

* Setting up different method for catching value of correct type.
2016-08-10 08:31:44 +02:00
Paulus Schoutsen 1726c4b45a Merge pull request #2776 from home-assistant/add-notify-test
Add notify demo data test
2016-08-09 21:47:11 -07:00
Paulus Schoutsen 9fa1328111 Move config validation exception logging to bootstrap + humanize 2016-08-09 21:38:44 -07:00
Paulus Schoutsen f904d06c9a Add test for new template validation logic 2016-08-09 21:09:56 -07:00
Paulus Schoutsen 253628da11 Add notify test being called from a YAML/script combi 2016-08-09 21:03:06 -07:00
Paulus Schoutsen e773526714 Template config validator should not allow dictionaries 2016-08-09 20:58:27 -07:00
Paulus Schoutsen 6dc49ff123 Humanize service call config validation errors 2016-08-09 20:58:08 -07:00
Paulus Schoutsen 6bb6a6ebe9 Add notify demo data test 2016-08-09 20:26:17 -07:00
Paulus Schoutsen 492ade7b1a Update frontend 2016-08-09 20:12:20 -07:00
D.-L.Pohl dc9f990ad2 Pilight component (#2742)
* New component to interface with a pilight-daemon for RF send/receive

* Fix bug that changed the received data, add connected flag, clean up

* New pilight switch component

* New optional whitelist filter to filter uninteressting devices

* Add pilight

* PEP8: too long lines, white spaces

* To keep up the good coverage ...

* PEP 257

* pylint enhancements

* pylint enhancements

* PEP 257

* Better HA config validation and cleanup following code review for #2742

* Fix requirenments to require fixed pilight version

* Change config validation to use voluptuous

* Pilight switch exclude not needed due to wildcard pilight exclude

* Enhance configuration parsing using voluptuous
2016-08-09 19:45:40 -07:00
Paulus Schoutsen d80c05b6b6 Enforce lower case for services and warn if local unknown service called (#2764) 2016-08-09 19:41:45 -07:00
Per Sandström 180a7ec295 add changed_by attribute to lock (#2766) 2016-08-09 19:37:46 -07:00
Pascal Vizeli 431f0fd236 update pyhomematic to version 0.1.11 (#2770) 2016-08-09 22:55:25 +02:00
schneefux 3d2830278a Hyperion: backwards compatibility (#2769) 2016-08-09 08:46:47 -07:00
Adam Mills 3ac9aaf025 Filter continuous values from logbook (#2761)
* Filter continuous values from logbook

* Test filter continuous values from logbook
2016-08-09 08:01:02 -07:00
Paulus Schoutsen 0b7b0e54ba Move unit system to util (#2763) 2016-08-08 20:42:25 -07:00
Paulus Schoutsen 640a8b5a7f Limit dependencies of HA core (#2762) 2016-08-08 20:21:40 -07:00
Teagan Glenn 915b9cb3eb Fix pydoc strings 2016-08-08 20:19:56 -06:00
Phil Hansen 88734f05c6 garage door rpi_gpio.py fix (#2759) 2016-08-08 19:03:23 -07:00
Robbie Trencheny 8e6dd62853 Add an OhmConnect sensor (#2758)
* Add an OhmConnect sensor

* use .get
2016-08-08 17:54:59 -07:00
Paulus Schoutsen 9948587401 Merge branch 'pr/2726' into dev 2016-08-08 17:42:34 -07:00
Nick Touran 3c2b4f5128 Added optional embedded image attachments to notify.smtp. (#2738)
* Added optional embedded image attachments to notify.smtp.

Also restructured a bit to minimize code duplication and add some tests.

* Fixed formatting errors.

* SMTP cleanups thanks to code review.
2016-08-08 17:36:49 -07:00
Johann Kellerman efe754636a Script to manage secrets stored in the keyring (#2743)
* Keyring script to get, set and delete secrets

* Add info & keyring version
2016-08-08 17:36:11 -07:00
Paulus Schoutsen 8081fe794e Add panel custom to load any webcomponent (#2747) 2016-08-08 17:35:46 -07:00
Paulus Schoutsen 9a575eb6d6 Link prefetch panels (#2748)
* Add link=prefetch to index.html

* Improve http request logging
2016-08-08 17:35:27 -07:00
Pascal Vizeli 98c77dc08f Add ffmpeg camera platform support (#2755) 2016-08-08 17:34:46 -07:00
Robbie Trencheny 991e292d7e Foursquare Component (#2723)
* Add a Foursquare component which accepts push notifications from Foursquare and provides a user checkin service

* @balloob requested fixes

* Sort .coveragerc list of components by name

* Revert "Sort .coveragerc list of components by name"

This reverts commit 997ae22576.

* Only sort Foursquare since I get conflicts otherwise

* Add Foursquare checkin service to services.yaml
2016-08-08 17:34:29 -07:00
Teagan Glenn 7e37634b54 Honeywell hvac mode (#2757)
* Add set hvac mode to honeywell us thermostat

* Add hvac service to the HoneywellRound entity

* Fix pydoc

* Add typing

* Typing to unit test
2016-08-08 17:32:53 -07:00
Pascal Vizeli 5445aafee7 update yahooweather to 0.5 (#2756) 2016-08-08 12:43:15 -07:00
John Arild Berentsen 7077103c4f General logmessage cleanup (#2753) 2016-08-08 20:05:45 +02:00
Daniel Høyer Iversen fc101fbbcb Merge pull request #2754 from home-assistant/flux_led_lib
update flux led library
2016-08-08 20:03:07 +02:00
Daniel 21ffe2ed9b update flux led library 2016-08-08 19:43:04 +02:00
Paulus Schoutsen 19fae75669 Fix broken remote test 2016-08-08 09:11:15 -07:00
Per Sandström 8568773e7d add changed_by attribute to alarm control panel (#2737) 2016-08-08 09:00:20 -07:00
Dean Camera 5ff9e59b79 Update to latest Plex API, add music support. (#2739)
* Update to latest Plex API, add music support.

* Fix PyLint errors.

* Update Plex sensor module to latest PlexAPI.

* Oops - update Python sensor import.

* According to PlexAPI docs, this is the new API for Plex Pass members.

* More pylint STFUs.

* Move pylint suppression.

* Use plexapi NA type directly.

* Pylint objects to short variable names.
2016-08-08 08:55:58 -07:00
Daniel Høyer Iversen 689939ab9d Add support color and brightness for flux light (#2750) 2016-08-08 08:47:02 -07:00
Sean Dague 8daaee702b bump proliphix library to 0.3.1 (#2751)
The 0.3.1 version of the library includes fixes for time syncing the
thermostat under the covers when needed. All changes are done on the
library side, we just need to bump the required level in home
assistant.
2016-08-08 08:03:12 -07:00
John Arild Berentsen e6ad2e8d91 Handling and improvements for zwave network (#2728) 2016-08-08 16:52:28 +02:00
Paulus Schoutsen dd0b9f2f36 Update frontend 2016-08-08 00:41:32 -07:00
Marcelo Moreira de Mello 0383da7af1 This patch makes use of the unit_system global configuration parameter to determine the mesurement system between 'metric' or 'imperial' for Fibit component. It also supports the fitbit accept-language when en_GB measurement is desired. (#2745) 2016-08-07 21:58:16 -07:00
Paulus Schoutsen b9b1d95514 Tweak panel parameters (#2746) 2016-08-07 21:56:17 -07:00
Daniel Høyer Iversen 23472cb44d Handle numeric device id for rfxtrx devices (#2740) 2016-08-07 17:15:39 -07:00
Fabian Heredia Montiel 0377338a81 Improvement typing (#2735)
* Fix: Circular dependencies of internal files

* Change: dt.date for Date and dt.datetime for DateTime

* Use NewType if available

* FIX: Wrong version test

* Remove: Date and DateTime types due to error

* Change to HomeAssistantType

* General Improvement of Typing

* Improve typing config_validation

* Improve typing script

* General Typing Improvements

* Improve NewType check

* Improve typing db_migrator

* Improve util/__init__ typing

* Improve helpers/location typing

* Regroup imports and remove pylint: disable=ungrouped-imports

* General typing improvements
2016-08-07 16:26:35 -07:00
Open Home Automation a3ca3e878b Added support for serial particulate matters sensors - serial_pm (#2571) 2016-08-07 22:14:01 +02:00
Paulus Schoutsen d1107a9cf3 Merge pull request #2731 from home-assistant/teagan-unit-system
Teagan unit system
2016-08-04 22:53:44 -07:00
Paulus Schoutsen 231656916c Address last comments 2016-08-04 22:44:37 -07:00
Teagan M. Glenn 26526ca57a Add unit system support
Add unit symbol constants

Initial unit system object

Import more constants

Pydoc for unit system file

Import constants for configuration validation

Unit system validation method

Typing for constants

Inches are valid lengths too

Typings

Change base class to dict - needed for remote api call serialization

Validation

Use dictionary keys

Defined unit systems

Update location util to use metric instead of us fahrenheit

Update constant imports

Import defined unit systems

Update configuration to use unit system

Update schema to use unit system

Update constants

Add imports to core for unit system and distance

Type for config

Default unit system

Convert distance from HASS instance

Update temperature conversion to use unit system

Update temperature conversion

Set unit system based on configuration

Set info unit system

Return unit system dictionary with config dictionary

Auto discover unit system

Update location test for use metric

Update forecast unit system

Update mold indicator unit system

Update thermostat unit system

Update thermostat demo test

Unit tests around unit system

Update test common hass configuration

Update configuration unit tests

There should always be a unit system!

Update core unit tests

Constants typing

Linting issues

Remove unused import

Update fitbit sensor to use application unit system

Update google travel time to use application unit system

Update configuration example

Update dht sensor

Update DHT temperature conversion to use the utility function

Update swagger config

Update my sensors metric flag

Update hvac component temperature conversion

HVAC conversion for temperature

Pull unit from sensor type map

Pull unit from sensor type map

Update the temper sensor unit

Update yWeather sensor unit

Update hvac demo unit test

Set unit test config unit system to metric

Use hass unit system length for default in proximity

Use the name of the system instead of temperature

Use constants from const

Unused import

Forecasted temperature

Fix calculation in case furthest distance is greater than 1000000 units

Remove unneeded constants

Set default length to km or miles

Use constants

Linting doesn't like importing just for typing

Fix reference

Test is expecting meters - set config to meters

Use constant

Use constant

PyDoc for unit test

Should be not in

Rename to units

Change unit system to be an object - not a dictionary

Return tuple in conversion

Move convert to temperature util

Temperature conversion is now in unit system

Update imports

Rename to units

Units is now an object

Use temperature util conversion

Unit system is now an object

Validate and convert unit system config

Return the scalar value in template distance

Test is expecting meters

Update unit tests around unit system

Distance util returns tuple

Fix location info test

Set units

Update unit tests

Convert distance

DOH

Pull out the scalar from the vector

Linting

I really hate python linting

Linting again

BLARG

Unit test documentation

Unit test around is metric flag

Break ternary statement into if/else blocks

Don't use dictionary - use members

is metric flag

Rename constants

Use is metric flag

Move constants to CONST file

Move to const file

Raise error if unit is not expected

Typing

No need to return unit since only performing conversion if it can work

Use constants

Line wrapping

Raise error if invalid value

Remove subscripts from conversion as they are no longer returned as tuples

No longer tuples

No longer tuples

Check for numeric type

Fix string format to use correct variable

Typing

Assert errors raised

Remove subscript

Only convert temperature if we know the unit

If no unit of measurement set - default to HASS config

Convert only if we know the unit

Remove subscription

Fix not in clause

Linting fixes

Wants a boolean

Clearer if-block

Check if the key is in the config first

Missed a couple expecting tuples

Backwards compatibility

No like-y ternary!

Error handling around state setting

Pretty unit system configuration validation

More tuple crap

Use is metric flag

Error handling around min/max temp

Explode if no unit

Pull unit from config

Celsius has a decimal

Unused import

Check if it's a temperature before we try to convert it to a temperature

Linting says too many statements - combine lat/long in a fairly reasonable manner

Backwards compatibility unit test

Better doc
2016-08-04 22:02:19 -07:00
Robby Grossman dfad8aa6dc Remove 'remove node (secure)' service; is not a specialized implementation in Python-OZW and standard removal works fine. (#2730) 2016-08-04 21:04:08 -07:00
Matthew Treinish 496972a587 Add option to heat_control component to set min cycle duration
This commit adds a new config option to the heat_control thermostat
component, min_cycle_duration. Some heaters and/or ACs don't like
being constantly cycled on and off. Prior to this patch the
heat_control component can end up cycling the switch quite
frequently. (depending on how quickly the temperature changes) The
new option added is used for setting a minimum duration that must
have elapsed in either the on or off state before the thermostat will
send the service call to cycle the switch. This should enable users to
hand tune how frequently heat_control can switch the device on or off
to best suit the device being used.
2016-08-04 12:37:08 -04:00
mmello ef3e7b28a9 Added whitelist option to InfluxDB to select the only entities that will be logged on InfluxDB (#2727) 2016-08-04 08:35:01 -07:00
Paulus Schoutsen 792154a6a7 Update frontend 2016-08-03 08:22:47 -07:00
Johann Kellerman 09262a36c4 Hide NewType ImportErrors (#2717)
* Hide NewType ImportErrors

* No more NewType
2016-08-03 08:21:30 -07:00
Matthew Treinish 94acda2a31 Add AC mode to heat_control component (#2719)
This commit adds a new option to the heat_control component, ac_mode.
When set to true, this treats the toggle device as a cooler instead
of a heater. The concept being if you have a window or in-wall ac
unit that doesn't have a built-in thermostat having the home assistant
implemented thermostat would be as useful as for space heaters.
2016-08-02 21:56:08 -07:00
William Scanlon b8492832a6 Convert null to 0 for temp % sensors (#2710) 2016-08-02 21:53:26 -07:00
Assaf Inbal bb22ad3064 Proxy requests to the media player's media image (#2693)
This is needed when the media server and UI client are not on the same network.
2016-08-02 18:31:15 -07:00
John Arild Berentsen e36c6b24ee Add secure inclusion of nodes for zwave network (#2715)
* Add secure inclusion of nodes for zwave network

* Add secure inclusion of nodes for zwave network
2016-08-02 20:17:10 +02:00
John Arild Berentsen ad0224e9aa Add start and stop for zwave network (#2709) 2016-08-02 19:08:04 +02:00
John Arild Berentsen 40d7361828 Implement of BARRIER_OPERATOR for garage door (#2712) 2016-08-02 18:05:38 +02:00
Paulus Schoutsen ab377f169d Upgrade to voluptuous 0.9.2 (#2692) 2016-08-02 00:14:13 -07:00
Tomi Tuhkanen 434a7d6975 Added VS Code config folder to gitignore (#2707) 2016-08-01 23:59:09 -07:00
Paulus Schoutsen 992be38b94 Upgrade netdisco (#2706) 2016-08-01 23:50:01 -07:00
Paulus Schoutsen f50c30bbba Merge pull request #2704 from home-assistant/hotfix-0-25-2
Hotfix 0 25 2
2016-08-01 20:58:27 -07:00
Paulus Schoutsen b1b14f0e83 Version bump to 0.25.2 2016-08-01 20:56:59 -07:00
Tobie Booth b51ba85a15 Reverts changes to ZWave lock status update (#2595) (#2696) 2016-08-01 20:56:43 -07:00
Paulus Schoutsen 29dbeeb41e Remove SQLAlchemy as core dependency (#2702) 2016-08-01 18:37:00 -07:00
Tobie Booth 8b57fd008f Reverts changes to ZWave lock status update (#2595) (#2696) 2016-08-01 08:08:24 -07:00
Assaf Inbal 51d5268f9f Added a screenshot to LG Netcast TVs (#2694) 2016-07-31 21:58:55 -07:00
Paulus Schoutsen 6f23869a89 Fix Mac OS install script (#2691) 2016-07-31 20:58:39 -07:00
Sean Dague 483b0045fc support cooling season in proliphix thermostat (#2689)
Instead of always assuming we want to change the heat, instead use the
setback attribute which sets heat / cool setback based on current HVAC
mode. This means that the proliphix thermostat will do sensible things
during cooling season.
2016-07-31 19:13:36 -07:00
Paulus Schoutsen 1856e0110b Update frontend 2016-07-31 19:07:06 -07:00
Paulus Schoutsen c608740382 Merge pull request #2688 from home-assistant/hotfix-0-25-1
Hotfix 0 25 1
2016-07-31 17:34:34 -07:00
Paulus Schoutsen 08e694cac3 Version bump to 0.25.1 2016-07-31 17:21:24 -07:00
Paulus Schoutsen 628eacc83e Rollback voluptuous to 0.8.9 (#2687) 2016-07-31 17:21:02 -07:00
Stephen Hoekstra ba72166333 Add 5 second timeout to Kodi connections (#2683) 2016-07-31 17:21:02 -07:00
Johann Kellerman 74f284d2d7 Close session after execute. (#2677) 2016-07-31 17:21:02 -07:00
Jesse Newland a81a8c2bdf Bring back delayed zwave value update behavior (#2674) 2016-07-31 17:21:02 -07:00
Paulus Schoutsen 3686a5ed56 Try to deflake discovery tests 2016-07-31 17:21:02 -07:00
Paulus Schoutsen e7ead73fad Rollback voluptuous to 0.8.9 (#2687) 2016-07-31 17:20:08 -07:00
HBDK a73c2e57a8 Added mired and kelvin mode to flux (#2665)
* Added mired and kelvin mode to flux

* changed as requested

* Renamed varible

* attempt to add test for new method in flux.py

* removed line to fix lint error
2016-07-31 16:55:48 -07:00
Paulus Schoutsen c39c10a088 update frontend 2016-07-31 16:39:07 -07:00
Fabian Affolter 72fc77b84d Upgrade fuzzywuzzy to 0.11.1 (#2685) 2016-07-31 15:00:52 -07:00
Paulus Schoutsen f4d6ce08e4 Update frontend 2016-07-31 14:47:01 -07:00
Johann Kellerman e9bd5d54ad Recorder typing & ensure DB ready on query (#2680)
* Recorder typing & wait on DB ready
2016-07-31 22:56:57 +02:00
Fabian Affolter 2871ab6bb0 Upgrade sendgrid to 3.1.10 (#2684) 2016-07-31 13:49:01 -07:00
Jesse Newland cfa69fef1e Add Docker test runner (#2673)
* Add docker test runner

* Move test Dockerfile into virtualization folder

* Don't build zwave in test environment
2016-07-31 13:48:41 -07:00
Fabian Affolter 5faba21b8c Upgrade python-nmap to 0.6.1 (#2681) 2016-07-31 13:47:46 -07:00
Fabian Affolter ca1cf44194 Upgrade cherrypy to 7.1.0 (#2682) 2016-07-31 13:47:34 -07:00
Stephen Hoekstra 125059c5ac Add 5 second timeout to Kodi connections (#2683) 2016-07-31 13:47:24 -07:00
Robbie Trencheny 63ba5044b3 Kill celcius with fire, replacing it with celsius, finally finishing what #1860 started (#2679) 2016-07-31 12:18:40 -07:00
Robbie Trencheny a93195610a Add alarm control panel services.yaml
...because I was almost done being bored
2016-07-31 11:49:30 -07:00
Robbie Trencheny d48f6676ab Update lock services.yaml
Was so bored I forgot some things :(
2016-07-31 11:45:57 -07:00
Robbie Trencheny 794205ad8d Add garage door services.yaml
...because I was somehow still bored
2016-07-31 11:34:18 -07:00
Robbie Trencheny 0e367ceec6 Add lock services.yaml
...because I was still bored
2016-07-31 11:31:50 -07:00
Robbie Trencheny 44b9771d8a Add rollershutter services.yaml
...because I was bored
2016-07-31 11:27:57 -07:00
Teagan Glenn 122581da7f Proximity unit of measure (#2659)
* Allow multiple proximities

* Distance conversion

* Add unit of measurement and conversion to proximity

* Shorten attribute name

* Fix get unit of measurement

* Fix the km <-> m conversion

* Add type check and errors

* first path unit test around distance utility

* Fix numeric type check

* Fix conversion type-os

* Actually set the exception thrown flag

* Test for exact conversion

* More descriptive variable names

* Update method invocation to match change in method name

* Missed a couple variables

* Line continuation

* Fix linting too many return issue

* Break out proximity setup for list of proximity and for single proximity device

* Pass hass to setup function

* Check if setup succeeded for each proximity component

* Change variable name

* Break out branches in convert to avoid too many branches linting error

* Remove disable lint line

* Variables for default properties

* Combine logic

* Test loading multiple proximities for 100% code coverage on proximity component

* Unit test to reach 100%
Fail to configure proximities missing devices

* Fail first before processing

* Combine return statements

* lstrip = bad Teagan

* Utilize string formating instead of concatenation

* Fix variable reference

* Typeo

* Clean up conversion to reduce complexity

* Update unit tests to match code changes on distance util

* Test non numeric value

* Private methods, value type has already been checked.
2016-07-31 10:20:56 -07:00
Johann Kellerman de7e27c92c Close session after execute. (#2677) 2016-07-31 10:10:30 -07:00
Paulus Schoutsen 89ec39f629 Update frontend 2016-07-31 00:43:28 -07:00
Jesse Newland e0cbb92c05 Bring back delayed zwave value update behavior (#2674) 2016-07-31 09:09:00 +02:00
Paulus Schoutsen b35c44ce04 Merge pull request #2671 from home-assistant/deflake-discovery-tests
Try to deflake discovery tests
2016-07-30 22:05:22 -07:00
Paulus Schoutsen bbff13afee Try to deflake discovery tests 2016-07-30 19:58:14 -07:00
Robbie Trencheny ecfcc1fd41 Update authorship information
Sorry @balloob :)
2016-07-30 13:03:54 -07:00
Paulus Schoutsen 86bbfb00ad Version bump to 0.25 2016-07-30 12:43:40 -07:00
Paulus Schoutsen af7f3bd455 Version bump to 0.26.0.dev0 2016-07-30 12:42:42 -07:00
Paulus Schoutsen 06a68d0c62 Merge pull request #2654 from home-assistant/dev
0.25
2016-07-30 11:33:41 -07:00
Paulus Schoutsen 99b27b1ec6 Update frontend 2016-07-30 11:22:44 -07:00
Paulus Schoutsen 1a64f14bea Add commented out default password (#2656) 2016-07-30 10:40:51 -07:00
Fabian Affolter 52a3aa1ca5 Add timeout (fixes #2661) (#2666) 2016-07-30 10:36:56 -07:00
Nolan Gilley a94e8f48e0 Install mysqlclient and psycopg2 (#2662)
I don't know if this is the right place for this, but I'm tired of having to install mysqlclient or psycopg2 after every docker update if I want to use mysql of postgres.
2016-07-30 10:30:14 -07:00
Scott O'Neil 822a263622 Fixing PEP257 issues in #2633 (#2658) 2016-07-30 10:30:13 +02:00
John caa7e770be Expand to respond to basic node events (#2615)
Allows zwave devices that can only push out basic set commands to be
captured by hass as zwave.node_events.
2016-07-29 21:56:03 +02:00
Paulus Schoutsen 48fbec0a49 Merge branch 'master' into dev 2016-07-29 12:17:50 -07:00
John Lindley b5fb382c1c Add group state for locks (#2647)
* Add group state for locks

Added  ", (STATE_LOCKED, STATE_UNLOCKED)" to _GROUP_TYPES

Don't have a working HA right now, so can't test..

* Modified from homeassistant.const import

* Removed white space

* Line length change

* Removed white space.. again!
2016-07-29 11:55:18 -07:00
Paulus Schoutsen d5e652d244 Update panel.html 2016-07-29 09:28:15 -07:00
Pascal Vizeli 548d154cd8 fix telegram bug (#2653) 2016-07-29 15:20:23 +02:00
Paulus Schoutsen 55624bcff9 Add custom panel example using React (#2651) 2016-07-29 00:49:58 -07:00
Nolan Gilley 3c51d2df0f load the last good state from db if speedtest data is None (#2645)
* load the last good state from db if speedtest data is None

* return if recorder is not available
2016-07-28 20:58:55 -07:00
Dean Camera ce3c89db6e Add MPC-HC Media Player Component (#2635)
* Initial media_player component for the MPC-HC web API.

* Update .coveragerc to exclude the MPC-HC media player component.

* We don't need a session for every HTTP fetch.

* Use host in configuration YAML to match Kodi component.

* Fix PyLint errors.

* Fix PEP8 errors and use more idiomatic Python to get dict() values.

* Add MPC-HC remote command capabilities for basic control.
2016-07-28 20:54:22 -07:00
Scott O'Neil bf3c0472bb Adding tests for sonos registration (#2633) 2016-07-28 20:40:58 -07:00
Adam Garcia 6a3c5b093b Update to group component to properly handle zone changes in tracked devices (#2631)
* pep8 fixes for group and test

* update to pass linting

* docstring fix.

* reduced length of docstring on test.
2016-07-28 20:40:25 -07:00
Nolan Gilley bce4be88dc check for error while running speedtest (#2643) 2016-07-28 09:25:31 -07:00
Paulus Schoutsen ec8802ec44 Update frontend 2016-07-28 09:22:15 -07:00
Fabian Affolter 6e5e97554b Merge pull request #2642 from fabaff/x10
Remove Awesome Light artefacts
2016-07-28 07:35:02 +02:00
Fabian Affolter 79783e01d7 Remove Awesoe Light artefacts 2016-07-28 07:04:12 +02:00
schneefux 26983aa646 Hyperion active (#2634)
* Hyperion lets you turn it on and off

* Update hyperion to use recent API functions

The plugin now gets the active color from the server.
Add a configuration option "default_color" to customize the turn_on color.
2016-07-27 21:11:12 -07:00
fotoetienne a0f72e3569 Add support for x10 lights (#2637)
* Add support for x10 lights

* X10 linting and add to .coveragerc
2016-07-27 20:54:02 -07:00
Paulus Schoutsen 1620680127 Use local timezone for log and history dates (#2622)
* Use local timezone for log and history dates

* home-assistant-js fix

* Submodule updates not included so travis can build

* Separate Date and DateTime http validators

* Include submodule reference

* Update frontend
2016-07-27 20:43:46 -07:00
Johann Kellerman 4f89230251 Update icloud to respect track=false. (#2640) 2016-07-27 20:38:55 -07:00
William Scanlon cdb6f3717d Removed Google Voice SMS notification support (#2628) 2016-07-27 20:37:07 -07:00
Fabian Heredia Montiel ae97218582 Improvement typing core (#2624)
* Add package typing

* Add util/location typing

* FIX: lint wrong order of imports

* Fix sometyping and add helpers/entity typing

* Mypy import trick

* Add asteroid to test requiremts to fix pylint issue

* Fix deprecated function isSet for is_set

* Add loader.py typing

* Improve typing bootstrap
2016-07-27 20:33:49 -07:00
Johann Kellerman 8c728d1b4e Update icloud device_tracker (#2614)
*  slugify() for dev_id (fixes #2162) [Keep space replacement to not impact known_devices.yaml]
*  pyicloud upgrade 0.9.1
*  config validation
*  Only poll icloud every 4 minutes...
*  Immediately pull device state on HASS start
*  Added new test with icloud char e' acute [chr(233)]
* Suppress pyicloud logging
2016-07-26 23:53:31 +02:00
Fabian Affolter fed2c33b54 Add get_config (#2627) 2016-07-26 08:50:38 -07:00
John Arild Berentsen b4990d61f9 Make sure zwave values are updated regardles of manual or frontend update, (#2595)
* Make sure values are updated regardles of manual or frontend update,

* Devices with set_switch command was not happy with fast updating.

* Binary triggersensors command was not happy with refreshed updating.
2016-07-26 08:26:40 +02:00
Cameron Bulock 0eac187d97 DirecTV Receiver Media Player Component (#2559)
* DirecTV receiver component

* styling cleanup

* Updated coveragerc and requirements all

* using string format

* linter fixes
2016-07-25 23:20:56 -07:00
Open Home Automation de6f49c06f Add the option to add additional tags when logging to InfluxDB (#2613) 2016-07-25 23:01:57 -07:00
Paulus Schoutsen f1632496f0 Allow circular dependency with discovery (#2616) 2016-07-25 22:49:10 -07:00
Nathan Henrie 9c76b30e24 Add timeout kwarg to call_service() and API.__call__() (#2612)
Fixes #2611

Adds a timeout kwarg to call_service and API.__call__ with default set
to 5 (as per previous behavior). Will not change existing behavior but
will allow remote Python API calls to specify a longer (or shorter)
timeout if they know that a script takes longer than 5 seconds to
return.
2016-07-25 22:35:33 -07:00
Paulus Schoutsen 78c298e563 Fix test to test Norway fix (#2626) 2016-07-25 22:02:12 -07:00
vladonemo 14707630ae Implementing set_hvac_mode for Nest (#2621) 2016-07-25 08:29:40 -07:00
Paulus Schoutsen 8ee4503d7c Exclude tests in dependencies in test dir from pytest (#2618) 2016-07-25 08:26:07 -07:00
Johann Kellerman 4195254280 Update Qwikswitch: fix typing, add validation, shutdown (#2603)
* Update Qwikswitch: fix typing, add validation, shutdown

* Delay startup listener, fix validation

* Fix workerpool errors
2016-07-23 17:03:29 -07:00
Open Home Automation 2484ee53b8 Knx thermostat (#2575)
* Major rewrite of the KNX multi address device. This class wasn't used before, but the new class will be the base for the LNX thermostat module

* newer KNXIP version needed as the previous version had a serious bug

* Update knxip to later version

* Added thermostat module

* First implementation of a KNX thermostat module

* Minor cleanup

* Removed unsed code
2016-07-23 13:54:20 -07:00
Johann Kellerman 4cf618334c Update recorder. (#2549)
* Update recorder.

models.py:
 - Use scoped_session in models.py to fix shutdown error
__init__.py:
 - Session _commit & retry method
 - Single session var for purge_data
 - Ensure single _INSTANCE
 - repeat purge every 2 days
 - show correct time in log_error

* _commit

* Restore models to old functionality, swap purge, remove _INSTANCE cleanup from tests, typing ignore Base class

* pylint

* Remove recorder from model unit test
2016-07-23 11:25:17 -07:00
Fabian Heredia Montiel d4f78e8552 Type Hints - Core/Utils/Helpers Part 1 (#2592)
* Fix deprecated(moved) import

* Add util/dt typing

* Green on mypy util/dt

* Fix some errors

* First part of yping util/yaml

* Add more typing to util/yaml
2016-07-23 11:07:08 -07:00
Neil Lathwood 34ca1dac7d Added Russound RNET support (#2591)
* Added Russound RNET support

* Fixed farcy issues

* Updated volume_level + fixed requirements_all.txt

* Updated syntax + changed variable
2016-07-23 10:51:56 -07:00
Fabian Affolter d808d90d26 Upgrade sendgrid to 3.0.7 (#2604) 2016-07-23 10:51:20 -07:00
Paulus Schoutsen 487f3b2951 Update frontend 2016-07-23 10:19:26 -07:00
Nicolas Graziano d202929de5 Float value for input slider (#2607)
* Allow input_slider value to be a float number.

* Change input_slider unit test to allow float number.
2016-07-23 09:53:16 -07:00
Fabian Affolter 6a189eb18d Merge pull request #2605 from rostved/readme-api-url-fix
Fixed REST API URL in readme.
2016-07-23 14:00:57 +02:00
Mikkel Rostved 67dada226a Fixed REST API URL in readme. 2016-07-23 12:51:25 +02:00
Fabian Affolter 57c2dea02d Add timestamp filters (#2596) 2016-07-22 19:47:43 -07:00
Fabian Affolter 3122c0279f Upgrade slacker to 0.9.24 (#2597) 2016-07-22 19:25:06 -07:00
Fabian Affolter 843e997292 Upgrade netdisco to 0.7.0 (#2598) 2016-07-22 19:24:51 -07:00
Fabian Affolter a3ff001eec Upgrade voluptuous to 0.9.1 (#2602) 2016-07-22 19:24:23 -07:00
John Arild Berentsen 8389a0abe3 Position fix, updating fix and start-stop for zwave rollershutter (#2594) 2016-07-22 10:01:40 +02:00
Paulus Schoutsen c21a956895 Speed up MyPy test (#2584) 2016-07-21 23:54:25 -07:00
Pascal Vizeli e5c42a676d Update pyhomematic to version 0.1.10 (#2589) 2016-07-21 20:49:30 +02:00
Paulus Schoutsen a513e1cc35 Update frontend 2016-07-21 08:41:48 -07:00
John Arild Berentsen a0d71c9cb2 Positioning issue for zwave rollershutter. fix for #2581 (#2587)
This fixes issue: #2486
2016-07-21 15:07:48 +02:00
John Arild Berentsen 3441170827 Missing Fortrezz siren fix for #2581 (#2586) 2016-07-21 12:46:15 +02:00
John Arild Berentsen c56fa7cfed Thermostat and hvac status fix for #2465 (#2585) 2016-07-21 12:20:43 +02:00
Paulus Schoutsen 2ea2a62d45 Update service worker 2016-07-20 23:40:40 -07:00
Fabian Heredia Montiel 08226a4864 Type Hints - __main__ (#2574)
* Add __main__ type hints

* Fix most errors of __main__

* Add ignore for script.run()

* Add type annotations for from_config_dict and from_config_file

* Fix errors

* Fix requirement error

* Add mypy type check to tests

* Enable travis typing check

* Messed up the tox deps

* Laxer type checker
2016-07-20 22:38:52 -07:00
Robbie Trencheny d570d38d5c Change path to favicon in GNTP
This broke when #2537 was merged.
2016-07-20 14:46:16 -07:00
Teagan Glenn ae5dfbdf55 Allow templates for delays in scripts (#2560) 2016-07-20 20:26:17 +02:00
William Scanlon 53f9809567 Wink water leak sensor (#2572) 2016-07-20 07:39:45 -07:00
John Arild Berentsen aed9ab0271 Added more binary sensor and switch classes. Ref.Pepper1 database (#2573) 2016-07-20 16:21:09 +02:00
Paulus Schoutsen 59029f2830 Update frontend 2016-07-19 23:36:52 -07:00
Scott O'Neil 46216c3bda Fix services registration, and adding schema util to sonos (#2558)
* Moving service registration into def so that it can be called for both discovery methods

* Adding use of schemas to sonos
2016-07-19 22:37:24 -07:00
Nathan Henrie aa079625d4 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-19 21:51:38 -07:00
Brent dee9244566 Move location lookup before zone checks. (#2557) 2016-07-19 19:51:14 -07:00
Daniel Høyer Iversen a6e95db618 MagicLight/Flux WiFi Color LED Light Component (#2534)
* Initial version for flux light

* Update version of flux_led library

* update flux led
2016-07-19 19:32:10 -07:00
Fredrik Haglund 8f04e03f73 Added support for luminance value (#2562) 2016-07-19 19:16:31 -07:00
Daniel Høyer Iversen d64dae8fcf Rfxtrx sensor (#2563)
* fire event rfxtrx sensor

* Add fire_event to rfxtrx sensor config

* Add test for rfxtrx fire event in sensor
2016-07-19 19:15:50 -07:00
Nolan Gilley 3dd869f0c2 expect a list of devices from config (#2567)
support multiple components. (#2565)
2016-07-19 19:14:41 -07:00
Greg Dowling e34bfb7381 Tidy / Refactor Vera (#2569)
* Add power attribute to switch.

* Move device_state_attributes into base class.

* Fix imports following refactor.

* Bump pyvera version - should add contributed support for older (UI5) version dimmers and locks.

* Refactor device lookup to be based on vera classes, push category back into library.

* Add generic power attribute, fix inherited class order bug.

* Tidy.
2016-07-19 19:13:33 -07:00
Paulus Schoutsen 7c431911d1 Update frontend 2016-07-19 02:37:22 -07:00
Paulus Schoutsen 5001c9729f Update frontend 2016-07-18 21:29:50 -07:00
John Arild Berentsen 32f228f984 zxt 120 has changed in ozw (#2551) 2016-07-18 16:20:17 +02:00
Paulus Schoutsen 541fffc7fa Update frontend 2016-07-17 23:23:31 -07:00
Paulus Schoutsen 389c13c891 Add ensure config script (#2548) 2016-07-17 15:24:42 -07:00
Daniel Zozin 027266ed8b Fix initialization state for GPIO switches configured with inverted logic (#2550)
When switches are configured to use inverted logic, the GPIO pins initial
state has to be inverted as well (set to HIGH)
2016-07-17 15:18:16 -07:00
Fabian Affolter ddcad275f7 Upgrade pytz to 2016.6.1 (#2541) 2016-07-17 13:07:11 -07:00
Fabian Affolter 64d5a328f3 Upgrade cherrypy to 6.1.1 (#2538) 2016-07-17 13:06:41 -07:00
Fabian Affolter 1b447fb56f Upgrade python-twitch to 1.3.0 (#2540) 2016-07-17 13:05:50 -07:00
Fabian Affolter 9bed64e9c0 Upgrade python-telegram-bot to 5.0.0 (#2542) 2016-07-17 13:05:38 -07:00
Dan 1da94928c6 Fix bug with imap sensor (#2546)
Fixed bug where the new connection was not saved when a reconnect
attempt was made; broadended the exception catching.
2016-07-17 13:02:14 -07:00
Fabian Affolter a8f34eb728 Merge pull request #2545 from deisi/acer_pyserial_update
repaired dependency of the acer projector switch
2016-07-17 18:08:17 +02:00
Malte 1002a1b7c9 run gen_requirements.py 2016-07-17 18:00:41 +02:00
Malte Deiseroth f261aac9cb repaired dependency of the acer projector switch 2016-07-17 16:45:58 +02:00
Daniel Høyer Iversen cfbc749000 Merge pull request #2539 from home-assistant/rfxtrx_tests
Rfxtrx tests
2016-07-17 11:34:36 +02:00
Daniel 98550b5465 rfxtrx light tests 2016-07-17 11:14:29 +02:00
Daniel 034f1b9499 rfxtrx switch tests 2016-07-17 10:27:27 +02:00
Daniel c79cd905fe rfxtrx sensor tests 2016-07-17 10:24:08 +02:00
Daniel 294883a174 rfxtrx core tests 2016-07-17 10:20:24 +02:00
Paulus Schoutsen f94319e7cb Merge pull request #2537 from home-assistant/frontend-panels
Frontend panels
2016-07-16 23:54:12 -07:00
Paulus Schoutsen 38c50c830f Fix linting errors 2016-07-16 23:45:38 -07:00
Paulus Schoutsen 925a623445 Build frontend 2016-07-16 23:24:17 -07:00
Paulus Schoutsen fd5aad1ee7 Add panel_iframe component 2016-07-16 23:21:34 -07:00
Paulus Schoutsen 22b4aebeb3 Add support for dynamic frontend panels 2016-07-16 23:21:34 -07:00
Fabian Affolter 35a57e1385 Prepare for next development cycle 2016-07-17 00:23:57 +02:00
455 changed files with 21961 additions and 4121 deletions
+36 -4
View File
@@ -4,6 +4,7 @@ source = homeassistant
omit =
homeassistant/__main__.py
homeassistant/scripts/*.py
homeassistant/helpers/typing.py
# omit pieces of code that rely on external devices being present
homeassistant/components/apcupsd.py
@@ -88,25 +89,41 @@ omit =
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
homeassistant/components/knx.py
homeassistant/components/switch/knx.py
homeassistant/components/binary_sensor/knx.py
homeassistant/components/thermostat/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/ffmpeg.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py
homeassistant/components/camera/generic.py
homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/heatmiser.py
homeassistant/components/climate/homematic.py
homeassistant/components/climate/knx.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py
homeassistant/components/cover/wink.py
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bluetooth_le_tracker.py
homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
@@ -122,28 +139,33 @@ omit =
homeassistant/components/discovery.py
homeassistant/components/downloader.py
homeassistant/components/feedreader.py
homeassistant/components/garage_door/wink.py
homeassistant/components/foursquare.py
homeassistant/components/garage_door/rpi_gpio.py
homeassistant/components/garage_door/wink.py
homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py
homeassistant/components/light/blinksticklight.py
homeassistant/components/light/flux_led.py
homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py
homeassistant/components/light/x10.py
homeassistant/components/lirc.py
homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/mpchc.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
@@ -151,6 +173,7 @@ omit =
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py
@@ -161,9 +184,10 @@ omit =
homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py
homeassistant/components/notify/googlevoice.py
homeassistant/components/notify/group.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
@@ -187,23 +211,31 @@ omit =
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gpsd.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/mhz19.py
homeassistant/components/sensor/mqtt_room.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/ohmconnect.py
homeassistant/components/sensor/onewire.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/openexchangerates.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py
+2
View File
@@ -0,0 +1,2 @@
.tox
.git
+10 -3
View File
@@ -7,9 +7,12 @@ config/custom_components/*
!config/custom_components/example.py
!config/custom_components/hello_world.py
!config/custom_components/mqtt_example.py
!config/panels
config/panels/*
!config/panels/react.html
tests/config/deps
tests/config/home-assistant.log
tests/testing_config/deps
tests/testing_config/home-assistant.log
# Hide sublime text stuff
*.sublime-project
@@ -51,7 +54,8 @@ develop-eggs
lib
lib64
# Installer logs
# Logs
*.log
pip-log.txt
# Unit test / coverage reports
@@ -90,3 +94,6 @@ ctags.tmp
virtualization/vagrant/setup_done
virtualization/vagrant/.vagrant
virtualization/vagrant/config
# Visual Studio Code
.vscode
+5
View File
@@ -8,8 +8,13 @@ matrix:
env: TOXENV=requirements
- python: "3.5"
env: TOXENV=lint
- python: "3.5"
env: TOXENV=typing
- python: "3.5"
env: TOXENV=py35
allow_failures:
- python: "3.5"
env: TOXENV=typing
cache:
directories:
- $HOME/.cache/pip
+2 -1
View File
@@ -20,7 +20,8 @@ RUN script/build_python_openzwave && \
COPY requirements_all.txt requirements_all.txt
# certifi breaks Debian based installs
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
pip3 install mysqlclient psycopg2
# Copy source
COPY . .
+1 -1
View File
@@ -67,7 +67,7 @@ Build home automation on top of your devices:
- 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/>`__
- Offers a `REST API <https://home-assistant.io/developers/rest_api/>`__
and can interface with MQTT for easy integration with other projects
like `OwnTracks <http://owntracks.org/>`__
- Allow sending notifications using
+2 -2
View File
@@ -10,8 +10,8 @@ homeassistant:
# Impacts weather/sunrise data
elevation: 665
# C for Celsius, F for Fahrenheit
temperature_unit: C
# 'metric' for Metric System, 'imperial' for imperial system
unit_system: metric
# Pick yours from here:
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+432
View File
@@ -0,0 +1,432 @@
<!--
Custom Home Assistant panel example.
Currently only works in Firefox and Chrome because it uses ES6.
Make sure this file is in <config>/panels/react.html
Add to your configuration.yaml:
panel_custom:
- name: react
sidebar_title: TodoMVC
sidebar_icon: mdi:checkbox-marked-outline
config:
title: Wow hello!
-->
<script src="https://fb.me/react-15.2.1.min.js"></script>
<script src="https://fb.me/react-dom-15.2.1.min.js"></script>
<!-- for development, replace with:
<script src="https://fb.me/react-15.2.1.js"></script>
<script src="https://fb.me/react-dom-15.2.1.js"></script>
-->
<!--
CSS taken from ReactJS TodoMVC example by Pete Hunt
http://todomvc.com/examples/react/
-->
<style>
.todoapp input[type="checkbox"] {
outline: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.todoapp .main {
position: relative;
border-top: 1px solid #e6e6e6;
}
.todoapp .todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todoapp .todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todoapp .todo-list li:last-child {
border-bottom: none;
}
.todoapp .todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
.todoapp .todo-list li .toggle:focus {
border-left: 3px solid rgba(175, 47, 47, 0.35);
}
.todoapp .todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todoapp .todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todoapp .todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todoapp .todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todoapp .footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.todoapp .footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todoapp .todo-count {
float: left;
text-align: left;
font-weight: 300;
}
.todoapp .toggle-menu {
position: absolute;
right: 15px;
font-weight: 300;
color: rgba(175, 47, 47, 0.75);
}
.todoapp .filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.todoapp .filters li {
display: inline;
}
.todoapp .filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.todoapp .filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.todoapp .filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.todoapp .toggle-all,
.todoapp .todo-list li .toggle {
background: none;
}
.todoapp .todo-list li .toggle {
height: 40px;
}
.todoapp .toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.todoapp .footer {
height: 50px;
}
.todoapp .filters {
bottom: 10px;
}
}
</style>
<dom-module id='ha-panel-react'>
<template>
<style>
:host {
background: #f5f5f5;
display: block;
height: 100%;
overflow: auto;
}
.mount {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
</style>
<div id='mount' class='mount'></div>
</template>
</dom-module>
<script>
// Example uses ES6. Will only work in modern browsers
class TodoMVC extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: 'all',
// load initial value of entities
entities: this.props.hass.reactor.evaluate(
this.props.hass.entityGetters.visibleEntityMap),
};
}
componentDidMount() {
// register to entity updates
this._unwatchHass = this.props.hass.reactor.observe(
this.props.hass.entityGetters.visibleEntityMap,
entities => this.setState({entities}))
}
componentWillUnmount() {
// unregister to entity updates
this._unwatchHass();
}
handlePickFilter(filter, ev) {
ev.preventDefault();
this.setState({filter});
}
handleEntityToggle(entity, ev) {
this.props.hass.serviceActions.callService(
entity.domain, 'toggle', { entity_id: entity.entityId });
}
handleToggleMenu(ev) {
ev.preventDefault();
Polymer.Base.fire('open-menu', null, {node: ev.target});
}
entityRow(entity) {
const completed = entity.state === 'on';
return React.createElement(
'li', {
className: completed && 'completed',
key: entity.entityId,
},
React.createElement(
"div", { className: "view" },
React.createElement(
"input", {
checked: completed,
className: "toggle",
type: "checkbox",
onChange: ev => this.handleEntityToggle(entity, ev),
}),
React.createElement("label", null, entity.entityDisplay)));
}
filterRow(filter) {
return React.createElement(
"li", { key: filter },
React.createElement(
"a", {
href: "#",
className: this.state.filter === filter && "selected",
onClick: ev => this.handlePickFilter(filter, ev),
},
filter.substring(0, 1).toUpperCase() + filter.substring(1)
)
);
}
render() {
const { entities, filter } = this.state;
if (!entities) return null;
const filters = ['all', 'light', 'switch'];
const showEntities = filter === 'all' ?
entities.filter(ent => filters.includes(ent.domain)) :
entities.filter(ent => ent.domain == filter);
return React.createElement(
'div', { className: 'todoapp-wrapper' },
React.createElement(
"section", { className: "todoapp" },
React.createElement(
"div", null,
React.createElement(
"header", { className: "header" },
React.createElement("h1", null, this.props.title || "todos")
),
React.createElement(
"section", { className: "main" },
React.createElement(
"ul", { className: "todo-list" },
showEntities.valueSeq().map(ent => this.entityRow(ent)))
)
),
React.createElement(
"footer", { className: "footer" },
React.createElement(
"span", { className: "todo-count" },
showEntities.filter(ent => ent.state === 'off').size + " items left"
),
React.createElement(
"ul", { className: "filters" },
filters.map(filter => this.filterRow(filter))
),
!this.props.showMenu && React.createElement(
"a", {
className: "toggle-menu",
href: '#',
onClick: ev => this.handleToggleMenu(ev),
},
"Show menu"
)
)
));
}
}
Polymer({
is: 'ha-panel-react',
properties: {
// Home Assistant object
hass: {
type: Object,
},
// If should render in narrow mode
narrow: {
type: Boolean,
value: false,
},
// If sidebar is currently shown
showMenu: {
type: Boolean,
value: false,
},
// Home Assistant panel info
// panel.config contains config passed to register_panel serverside
panel: {
type: Object,
}
},
// This will make sure we forward changed properties to React
observers: [
'propsChanged(hass, narrow, showMenu, panel)',
],
// Mount React when element attached
attached: function () {
this.mount(this.hass, this.narrow, this.showMenu, this.panel);
},
// Called when properties change
propsChanged: function (hass, narrow, showMenu, panel) {
this.mount(hass, narrow, showMenu, panel);
},
// Render React. Debounce in case multiple properties change.
mount: function (hass, narrow, showMenu, panel) {
this.debounce('mount', function () {
ReactDOM.render(React.createElement(TodoMVC, {
hass: hass,
narrow: narrow,
showMenu: showMenu,
title: panel.config ? panel.config.title : null
}), this.$.mount);
}.bind(this));
},
// Unmount React node when panel no longer in use.
detached: function () {
ReactDOM.unmountComponentAtNode(this.$.mount);
},
});
</script>
+2 -1
View File
@@ -419,8 +419,9 @@ definitions:
description: Longitude of Home Assistant server
location_name:
type: string
temperature_unit:
unit_system:
type: string
description: The system for measurement units
time_zone:
type: string
version:
+20 -17
View File
@@ -8,6 +8,8 @@ import subprocess
import sys
import threading
from typing import Optional, List
from homeassistant.const import (
__version__,
EVENT_HOMEASSISTANT_START,
@@ -16,7 +18,7 @@ from homeassistant.const import (
)
def validate_python():
def validate_python() -> None:
"""Validate we're running the right Python version."""
major, minor = sys.version_info[:2]
req_major, req_minor = REQUIRED_PYTHON_VER
@@ -27,7 +29,7 @@ def validate_python():
sys.exit(1)
def ensure_config_path(config_dir):
def ensure_config_path(config_dir: str) -> None:
"""Validate the configuration directory."""
import homeassistant.config as config_util
lib_dir = os.path.join(config_dir, 'deps')
@@ -56,7 +58,7 @@ def ensure_config_path(config_dir):
sys.exit(1)
def ensure_config_file(config_dir):
def ensure_config_file(config_dir: str) -> str:
"""Ensure configuration file exists."""
import homeassistant.config as config_util
config_path = config_util.ensure_config_exists(config_dir)
@@ -68,7 +70,7 @@ def ensure_config_file(config_dir):
return config_path
def get_arguments():
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
import homeassistant.config as config_util
parser = argparse.ArgumentParser(
@@ -125,12 +127,12 @@ def get_arguments():
arguments = parser.parse_args()
if os.name != "posix" or arguments.debug or arguments.runner:
arguments.daemon = False
setattr(arguments, 'daemon', False)
return arguments
def daemonize():
def daemonize() -> None:
"""Move current process to daemon process."""
# Create first fork
pid = os.fork()
@@ -155,7 +157,7 @@ def daemonize():
os.dup2(outfd.fileno(), sys.stderr.fileno())
def check_pid(pid_file):
def check_pid(pid_file: str) -> None:
"""Check that HA is not already running."""
# Check pid file
try:
@@ -177,7 +179,7 @@ def check_pid(pid_file):
sys.exit(1)
def write_pid(pid_file):
def write_pid(pid_file: str) -> None:
"""Create a PID File."""
pid = os.getpid()
try:
@@ -187,7 +189,7 @@ def write_pid(pid_file):
sys.exit(1)
def closefds_osx(min_fd, max_fd):
def closefds_osx(min_fd: int, max_fd: int) -> None:
"""Make sure file descriptors get closed when we restart.
We cannot call close on guarded fds, and we cannot easily test which fds
@@ -205,7 +207,7 @@ def closefds_osx(min_fd, max_fd):
pass
def cmdline():
def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith('/__main__.py'):
modulepath = os.path.dirname(sys.argv[0])
@@ -213,16 +215,17 @@ def cmdline():
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir, args):
def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> Optional[int]:
"""Setup HASS and run."""
from homeassistant import bootstrap
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
args = cmdline() + ['--runner']
nt_args = cmdline() + ['--runner']
while True:
try:
subprocess.check_call(args)
subprocess.check_call(nt_args)
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
@@ -244,7 +247,7 @@ def setup_and_run_hass(config_dir, args):
log_rotate_days=args.log_rotate_days)
if hass is None:
return
return None
if args.open_ui:
def open_browser(event):
@@ -261,7 +264,7 @@ def setup_and_run_hass(config_dir, args):
return exit_code
def try_to_restart():
def try_to_restart() -> None:
"""Attempt to clean up state and start a new homeassistant instance."""
# Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind.
@@ -271,7 +274,7 @@ def try_to_restart():
# thread left (which is us). Nothing we really do with it, but it might be
# useful when debugging shutdown/restart issues.
try:
nthreads = sum(thread.isAlive() and not thread.isDaemon()
nthreads = sum(thread.is_alive() and not thread.daemon
for thread in threading.enumerate())
if nthreads > 1:
sys.stderr.write(
@@ -303,7 +306,7 @@ def try_to_restart():
os.execv(args[0], args)
def main():
def main() -> int:
"""Start Home Assistant."""
validate_python()
+67 -22
View File
@@ -7,15 +7,19 @@ import sys
from collections import defaultdict
from threading import RLock
from types import ModuleType
from typing import Any, Optional, Dict
import voluptuous as vol
from voluptuous.humanize import humanize_error
import homeassistant.components as core_components
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.package as pkg_util
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
@@ -30,7 +34,8 @@ ATTR_COMPONENT = 'component'
ERROR_LOG_FILENAME = 'home-assistant.log'
def setup_component(hass, domain, config=None):
def setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies."""
if domain in hass.config.components:
return True
@@ -53,7 +58,8 @@ def setup_component(hass, domain, config=None):
return True
def _handle_requirements(hass, component, name):
def _handle_requirements(hass: core.HomeAssistant, component,
name: str) -> bool:
"""Install the requirements for a component."""
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
return True
@@ -67,9 +73,10 @@ def _handle_requirements(hass, component, name):
return True
def _setup_component(hass, domain, config):
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
"""Setup a component for Home Assistant."""
# pylint: disable=too-many-return-statements,too-many-branches
# pylint: disable=too-many-statements
if domain in hass.config.components:
return True
@@ -97,7 +104,7 @@ def _setup_component(hass, domain, config):
try:
config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, domain, config)
log_exception(ex, domain, config)
return False
elif hasattr(component, 'PLATFORM_SCHEMA'):
@@ -107,7 +114,7 @@ def _setup_component(hass, domain, config):
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, domain, p_config)
log_exception(ex, domain, p_config)
return False
# Not all platform components follow same pattern for platforms
@@ -128,8 +135,8 @@ def _setup_component(hass, domain, config):
try:
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, '{}.{}'
.format(domain, p_name), p_validated)
log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated)
return False
platforms.append(p_validated)
@@ -147,9 +154,15 @@ def _setup_component(hass, domain, config):
_CURRENT_SETUP.append(domain)
try:
if not component.setup(hass, config):
result = component.setup(hass, config)
if result is False:
_LOGGER.error('component %s failed to initialize', domain)
return False
elif result is not True:
_LOGGER.error('component %s did not return boolean if setup '
'was successful. Disabling component.', domain)
loader.set_component(domain, None)
return False
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error during setup of component %s', domain)
return False
@@ -169,7 +182,8 @@ def _setup_component(hass, domain, config):
return True
def prepare_setup_platform(hass, config, domain, platform_name):
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) -> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup."""
_ensure_loader_prepared(hass)
@@ -202,9 +216,14 @@ def prepare_setup_platform(hass, config, domain, platform_name):
# 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,
log_rotate_days=None):
def from_config_dict(config: Dict[str, Any],
hass: Optional[core.HomeAssistant]=None,
config_dir: Optional[str]=None,
enable_log: bool=True,
verbose: bool=False,
skip_pip: bool=False,
log_rotate_days: Any=None) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a config dict.
Dynamically loads required components and its dependencies.
@@ -214,14 +233,14 @@ 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:
conf_util.process_ha_core_config(hass, core_config)
except vol.Invalid as ex:
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
log_exception(ex, 'homeassistant', core_config)
return None
conf_util.process_ha_config_upgrade(hass)
@@ -266,8 +285,11 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
return hass
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
log_rotate_days=None):
def from_config_file(config_path: str,
hass: Optional[core.HomeAssistant]=None,
verbose: bool=False,
skip_pip: bool=True,
log_rotate_days: Any=None):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@@ -279,7 +301,7 @@ 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)
@@ -287,12 +309,15 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
config_dict = conf_util.load_yaml_config_file(config_path)
except HomeAssistantError:
return None
finally:
clear_secret_cache()
return from_config_dict(config_dict, hass, enable_log=False,
skip_pip=skip_pip)
def enable_logging(hass, verbose=False, log_rotate_days=None):
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
log_rotate_days=None) -> None:
"""Setup the logging."""
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
@@ -343,12 +368,32 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
'Unable to setup error log %s (access denied)', err_log_path)
def _ensure_loader_prepared(hass):
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
"""Ensure Home Assistant loader is prepared."""
if not loader.PREPARED:
loader.prepare(hass)
def _mount_local_lib_path(config_dir):
def log_exception(ex, domain, config):
"""Generate log exception for config validation."""
message = 'Invalid config for [{}]: '.format(domain)
if 'extra keys not allowed' in ex.error_message:
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
.format(ex.path[-1], domain, domain,
'->'.join('%s' % m for m in ex.path))
else:
message += humanize_error(config, ex)
if hasattr(config, '__line__'):
message += " (See {}:{})".format(config.__config_file__,
config.__line__ or '?')
_LOGGER.error(message)
def mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path."""
sys.path.insert(0, os.path.join(config_dir, 'deps'))
deps_dir = os.path.join(config_dir, 'deps')
if deps_dir not in sys.path:
sys.path.insert(0, os.path.join(config_dir, 'deps'))
return deps_dir
+2 -3
View File
@@ -11,7 +11,6 @@ import itertools as it
import logging
import homeassistant.core as ha
from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers.service import extract_entity_ids
from homeassistant.loader import get_component
from homeassistant.const import (
@@ -35,7 +34,7 @@ def is_on(hass, entity_id=None):
entity_ids = hass.states.entity_ids()
for entity_id in entity_ids:
domain = split_entity_id(entity_id)[0]
domain = ha.split_entity_id(entity_id)[0]
module = get_component(domain)
@@ -95,7 +94,7 @@ def setup(hass, config):
# Group entity_ids by domain. groupby requires sorted data.
by_domain = it.groupby(sorted(entity_ids),
lambda item: split_entity_id(item)[0])
lambda item: ha.split_entity_id(item)[0])
for domain, ent_ids in by_domain:
# We want to block for all calls and only return when all calls
@@ -20,6 +20,7 @@ from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel'
SCAN_INTERVAL = 30
ATTR_CHANGED_BY = 'changed_by'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@@ -124,6 +125,11 @@ class AlarmControlPanel(Entity):
"""Regex for code format or None if no code is required."""
return None
@property
def changed_by(self):
"""Last change triggered by."""
return None
def alarm_disarm(self, code=None):
"""Send disarm command."""
raise NotImplementedError()
@@ -145,5 +151,6 @@ class AlarmControlPanel(Entity):
"""Return the state attributes."""
state_attr = {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by
}
return state_attr
@@ -80,7 +80,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
def alarm_disarm(self, code=None):
"""Send disarm command."""
if not self._validate_code(code, 'arming home'):
if not self._validate_code(code, 'disarming home'):
return
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
# Open another session to alarm.com to fire off the command
@@ -10,5 +10,5 @@ import homeassistant.components.alarm_control_panel.manual as manual
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo alarm control panel platform."""
add_devices([
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10),
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False),
])
@@ -7,28 +7,46 @@ https://home-assistant.io/components/alarm_control_panel.manual/
import datetime
import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME,
CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time
_LOGGER = logging.getLogger(__name__)
DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_PENDING_TIME = 60
DEFAULT_TRIGGER_TIME = 120
DEFAULT_DISARM_AFTER_TRIGGER = False
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'manual',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
})
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the manual alarm platform."""
add_devices([ManualAlarm(
hass,
config.get('name', DEFAULT_ALARM_NAME),
config.get('code'),
config.get('pending_time', DEFAULT_PENDING_TIME),
config.get('trigger_time', DEFAULT_TRIGGER_TIME),
config[CONF_NAME],
config.get(CONF_CODE),
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER)
)])
@@ -40,10 +58,12 @@ class ManualAlarm(alarm.AlarmControlPanel):
When armed, will be pending for 'pending_time', after that armed.
When triggered, will be pending for 'trigger_time'. After that will be
triggered for 'trigger_time', after that we return to disarmed.
triggered for 'trigger_time', after that we return to the previous state
or disarm if `disarm_after_trigger` is true.
"""
def __init__(self, hass, name, code, pending_time, trigger_time):
def __init__(self, hass, name, code, pending_time,
trigger_time, disarm_after_trigger):
"""Initalize the manual alarm panel."""
self._state = STATE_ALARM_DISARMED
self._hass = hass
@@ -51,6 +71,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._code = str(code) if code else None
self._pending_time = datetime.timedelta(seconds=pending_time)
self._trigger_time = datetime.timedelta(seconds=trigger_time)
self._disarm_after_trigger = disarm_after_trigger
self._pre_trigger_state = self._state
self._state_ts = None
@property
@@ -77,7 +99,10 @@ class ManualAlarm(alarm.AlarmControlPanel):
return STATE_ALARM_PENDING
elif (self._state_ts + self._pending_time +
self._trigger_time) < dt_util.utcnow():
return STATE_ALARM_DISARMED
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
else:
return self._pre_trigger_state
return self._state
@@ -125,6 +150,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
def alarm_trigger(self, code=None):
"""Send alarm trigger command. No code needed."""
self._pre_trigger_state = self._state
self._state = STATE_ALARM_TRIGGERED
self._state_ts = dt_util.utcnow()
self.update_ha_state()
@@ -0,0 +1,43 @@
alarm_disarm:
description: Send the alarm the command for disarm
fields:
entity_id:
description: Name of alarm control panel to disarm
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to disarm the alarm control panel with
example: 1234
alarm_arm_home:
description: Send the alarm the command for arm home
fields:
entity_id:
description: Name of alarm control panel to arm home
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to arm home the alarm control panel with
example: 1234
alarm_arm_away:
description: Send the alarm the command for arm away
fields:
entity_id:
description: Name of alarm control panel to arm away
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to arm away the alarm control panel with
example: 1234
alarm_trigger:
description: Send the alarm the command for trigger
fields:
entity_id:
description: Name of alarm control panel to trigger
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to trigger the alarm control panel with
example: 1234
@@ -37,6 +37,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
self._id = device_id
self._state = STATE_UNKNOWN
self._digits = int(hub.config.get('code_digits', '4'))
self._changed_by = None
@property
def name(self):
@@ -58,6 +59,11 @@ class VerisureAlarm(alarm.AlarmControlPanel):
"""The code format as regex."""
return '^\\d{%s}$' % self._digits
@property
def changed_by(self):
"""Last change triggered by."""
return self._changed_by
def update(self):
"""Update alarm status."""
hub.update_alarms()
@@ -72,6 +78,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
_LOGGER.error(
'Unknown alarm state %s',
hub.alarm_status[self._id].status)
self._changed_by = hub.alarm_status[self._id].name
def alarm_disarm(self, code=None):
"""Send disarm command."""
+6 -6
View File
@@ -11,17 +11,17 @@ from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'alexa'
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
API_ENDPOINT = '/api/alexa'
CONF_INTENTS = 'intents'
CONF_CARD = 'card'
CONF_SPEECH = 'speech'
CONF_ACTION = 'action'
CONF_CARD = 'card'
CONF_INTENTS = 'intents'
CONF_SPEECH = 'speech'
DOMAIN = 'alexa'
DEPENDENCIES = ['http']
def setup(hass, config):
+22 -14
View File
@@ -7,35 +7,43 @@ https://home-assistant.io/components/apcupsd/
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
DOMAIN = "apcupsd"
REQUIREMENTS = ("apcaccess==0.0.4",)
REQUIREMENTS = ['apcaccess==0.0.4']
CONF_HOST = "host"
CONF_PORT = "port"
CONF_TYPE = "type"
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = "localhost"
CONF_TYPE = 'type'
DATA = None
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 3551
DOMAIN = 'apcupsd'
KEY_STATUS = "STATUS"
VALUE_ONLINE = "ONLINE"
KEY_STATUS = 'STATUS'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
DATA = None
VALUE_ONLINE = 'ONLINE'
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Use config values to set up a function enabling status retrieval."""
global DATA
host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST)
port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT)
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
port = conf.get(CONF_PORT)
DATA = APCUPSdData(host, port)
+16 -9
View File
@@ -6,27 +6,34 @@ https://home-assistant.io/components/arduino/
"""
import logging
import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import validate_config
from homeassistant.const import CONF_PORT
import homeassistant.helpers.config_validation as cv
DOMAIN = "arduino"
REQUIREMENTS = ['PyMata==2.12']
BOARD = None
_LOGGER = logging.getLogger(__name__)
BOARD = None
DOMAIN = 'arduino'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PORT): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup the Arduino component."""
if not validate_config(config,
{DOMAIN: ['port']},
_LOGGER):
return False
import serial
global BOARD
try:
BOARD = ArduinoBoard(config[DOMAIN]['port'])
BOARD = ArduinoBoard(config[DOMAIN][CONF_PORT])
except (serial.serialutil.SerialException, FileNotFoundError):
_LOGGER.exception("Your port is not accessible.")
return False
@@ -6,6 +6,8 @@ https://home-assistant.io/components/binary_sensor/
"""
import logging
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF)
@@ -33,6 +35,8 @@ SENSOR_CLASSES = [
'vibration', # On means vibration detected, Off means no vibration
]
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
def setup(hass, config):
"""Track states and offer events for binary sensors."""
@@ -4,23 +4,32 @@ Support for tracking the online status of a UPS.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.apcupsd/
"""
from homeassistant.components import apcupsd
from homeassistant.components.binary_sensor import BinarySensorDevice
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.components import apcupsd
DEFAULT_NAME = 'UPS Online Status'
DEPENDENCIES = [apcupsd.DOMAIN]
DEFAULT_NAME = "UPS Online Status"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Instantiate an OnlineStatus binary sensor entity."""
"""Setup an Online Status binary sensor."""
add_entities((OnlineStatus(config, apcupsd.DATA),))
class OnlineStatus(BinarySensorDevice):
"""Represent UPS online status."""
"""Representation of an UPS online status."""
def __init__(self, config, data):
"""Initialize the APCUPSd device."""
"""Initialize the APCUPSd binary device."""
self._config = config
self._data = data
self._state = None
@@ -29,7 +38,7 @@ class OnlineStatus(BinarySensorDevice):
@property
def name(self):
"""Return the name of the UPS online status sensor."""
return self._config.get("name", DEFAULT_NAME)
return self._config.get(CONF_NAME)
@property
def is_on(self):
@@ -0,0 +1,72 @@
"""
Support for Ecobee sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.ecobee/
"""
from homeassistant.components import ecobee
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['ecobee']
ECOBEE_CONFIG_FILE = 'ecobee.conf'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Ecobee sensors."""
if discovery_info is None:
return
data = ecobee.NETWORK
dev = list()
for index in range(len(data.ecobee.thermostats)):
for sensor in data.ecobee.get_remote_sensors(index):
for item in sensor['capability']:
if item['type'] != 'occupancy':
continue
dev.append(EcobeeBinarySensor(sensor['name'], index))
add_devices(dev)
class EcobeeBinarySensor(BinarySensorDevice):
"""Representation of an Ecobee sensor."""
def __init__(self, sensor_name, sensor_index):
"""Initialize the sensor."""
self._name = sensor_name + ' Occupancy'
self.sensor_name = sensor_name
self.index = sensor_index
self._state = None
self._sensor_class = 'motion'
self.update()
@property
def name(self):
"""Return the name of the Ecobee sensor."""
return self._name.rstrip()
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state == 'true'
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
return "binary_sensor_ecobee_{}_{}".format(self._name, self.index)
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return self._sensor_class
def update(self):
"""Get the latest state of the sensor."""
data = ecobee.NETWORK
data.update()
for sensor in data.ecobee.get_remote_sensors(self.index):
for item in sensor['capability']:
if (item['type'] == 'occupancy' and
self.sensor_name == sensor['name']):
self._state = item['value']
@@ -4,27 +4,41 @@ Support for EnOcean binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.enocean/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
from homeassistant.components import enocean
from homeassistant.const import CONF_NAME
from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SENSOR_CLASS)
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ["enocean"]
_LOGGER = logging.getLogger(__name__)
CONF_ID = "id"
DEPENDENCIES = ['enocean']
DEFAULT_NAME = 'EnOcean binary sensor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Binary Sensor platform fo EnOcean."""
dev_id = config.get(CONF_ID, None)
devname = config.get(CONF_NAME, "EnOcean binary sensor")
add_devices([EnOceanBinarySensor(dev_id, devname)])
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
sensor_class = config.get(CONF_SENSOR_CLASS)
add_devices([EnOceanBinarySensor(dev_id, devname, sensor_class)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
"""Representation of EnOcean binary sensors such as wall switches."""
def __init__(self, dev_id, devname):
def __init__(self, dev_id, devname, sensor_class):
"""Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self)
self.stype = "listener"
@@ -32,12 +46,18 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
self.which = -1
self.onoff = -1
self.devname = devname
self._sensor_class = sensor_class
@property
def name(self):
"""The default name for the binary sensor."""
return self.devname
@property
def sensor_class(self):
"""Return the class of this sensor."""
return self._sensor_class
def value_changed(self, value, value2):
"""Fire an event with the data that have changed.
@@ -0,0 +1,215 @@
"""
Provides a binary sensor which is a collection of ffmpeg tools.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ffmpeg/
"""
import logging
from os import path
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (BinarySensorDevice,
PLATFORM_SCHEMA, DOMAIN)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME,
ATTR_ENTITY_ID)
REQUIREMENTS = ["ha-ffmpeg==0.9"]
SERVICE_RESTART = 'ffmpeg_restart'
FFMPEG_SENSOR_NOISE = 'noise'
FFMPEG_SENSOR_MOTION = 'motion'
MAP_FFMPEG_BIN = [
FFMPEG_SENSOR_NOISE,
FFMPEG_SENSOR_MOTION
]
CONF_TOOL = 'tool'
CONF_INPUT = 'input'
CONF_FFMPEG_BIN = 'ffmpeg_bin'
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
CONF_OUTPUT = 'output'
CONF_PEAK = 'peak'
CONF_DURATION = 'duration'
CONF_RESET = 'reset'
CONF_CHANGES = 'changes'
CONF_REPEAT = 'repeat'
CONF_REPEAT_TIME = 'repeat_time'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN),
vol.Required(CONF_INPUT): cv.string,
vol.Optional(CONF_FFMPEG_BIN, default="ffmpeg"): cv.string,
vol.Optional(CONF_NAME, default="FFmpeg"): cv.string,
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
vol.Optional(CONF_OUTPUT): cv.string,
vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int),
vol.Optional(CONF_DURATION, default=1):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_RESET, default=10):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_CHANGES, default=10):
vol.All(vol.Coerce(float), vol.Range(min=0, max=99)),
vol.Optional(CONF_REPEAT, default=0):
vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.Optional(CONF_REPEAT_TIME, default=0):
vol.All(vol.Coerce(int), vol.Range(min=0)),
})
SERVICE_RESTART_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
# list of all ffmpeg sensors
DEVICES = []
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the binary sensor."""
from haffmpeg import SensorNoise, SensorMotion
if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE:
entity = FFmpegNoise(SensorNoise, config)
else:
entity = FFmpegMotion(SensorMotion, config)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, entity.shutdown_ffmpeg)
# add to system
add_entities([entity])
DEVICES.append(entity)
# exists service?
if hass.services.has_service(DOMAIN, SERVICE_RESTART):
return True
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
# register service
def _service_handle_restart(service):
"""Handle service binary_sensor.ffmpeg_restart."""
entity_ids = service.data.get('entity_id')
if entity_ids:
_devices = [device for device in DEVICES
if device.entity_id in entity_ids]
else:
_devices = DEVICES
for device in _devices:
device.reset_ffmpeg()
hass.services.register(DOMAIN, SERVICE_RESTART,
_service_handle_restart,
descriptions.get(SERVICE_RESTART),
schema=SERVICE_RESTART_SCHEMA)
return True
class FFmpegBinarySensor(BinarySensorDevice):
"""A binary sensor which use ffmpeg for noise detection."""
def __init__(self, ffobj, config):
"""Constructor for binary sensor noise detection."""
self._state = False
self._config = config
self._name = config.get(CONF_NAME)
self._ffmpeg = ffobj(config.get(CONF_FFMPEG_BIN), self._callback)
self._start_ffmpeg(config)
def _callback(self, state):
"""HA-FFmpeg callback for noise detection."""
self._state = state
self.update_ha_state()
def _start_ffmpeg(self, config):
"""Start a FFmpeg instance."""
raise NotImplementedError
def shutdown_ffmpeg(self, event):
"""For STOP event to shutdown ffmpeg."""
self._ffmpeg.close()
def reset_ffmpeg(self):
"""Restart ffmpeg with new config."""
self._ffmpeg.close()
self._start_ffmpeg(self._config)
@property
def is_on(self):
"""True if the binary sensor is on."""
return self._state
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def available(self):
"""Return True if entity is available."""
return self._ffmpeg.is_running
class FFmpegNoise(FFmpegBinarySensor):
"""A binary sensor which use ffmpeg for noise detection."""
def _start_ffmpeg(self, config):
"""Start a FFmpeg instance."""
# init config
self._ffmpeg.set_options(
time_duration=config.get(CONF_DURATION),
time_reset=config.get(CONF_RESET),
peak=config.get(CONF_PEAK),
)
# run
self._ffmpeg.open_sensor(
input_source=config.get(CONF_INPUT),
output_dest=config.get(CONF_OUTPUT),
extra_cmd=config.get(CONF_EXTRA_ARGUMENTS),
)
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return "sound"
class FFmpegMotion(FFmpegBinarySensor):
"""A binary sensor which use ffmpeg for noise detection."""
def _start_ffmpeg(self, config):
"""Start a FFmpeg instance."""
# init config
self._ffmpeg.set_options(
time_reset=config.get(CONF_RESET),
time_repeat=config.get(CONF_REPEAT_TIME),
repeat=config.get(CONF_REPEAT),
changes=config.get(CONF_CHANGES),
)
# run
self._ffmpeg.open_sensor(
input_source=config.get(CONF_INPUT),
extra_cmd=config.get(CONF_EXTRA_ARGUMENTS),
)
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return "motion"
@@ -20,7 +20,9 @@ SENSOR_TYPES_CLASS = {
"SmokeV2": "smoke",
"Motion": "motion",
"MotionV2": "motion",
"RemoteMotion": None
"RemoteMotion": None,
"WeatherSensor": None,
"TiltSensor": None,
}
@@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pres.S_MOTION: [set_req.V_TRIPPED],
pres.S_SMOKE: [set_req.V_TRIPPED],
}
if float(gateway.version) >= 1.5:
if float(gateway.protocol_version) >= 1.5:
map_sv_types.update({
pres.S_SPRINKLER: [set_req.V_TRIPPED],
pres.S_WATER_LEAK: [set_req.V_TRIPPED],
@@ -66,7 +66,7 @@ class MySensorsBinarySensor(
pres.S_MOTION: 'motion',
pres.S_SMOKE: 'smoke',
}
if float(self.gateway.version) >= 1.5:
if float(self.gateway.protocol_version) >= 1.5:
class_map.update({
pres.S_SPRINKLER: 'sprinkler',
pres.S_WATER_LEAK: 'leak',
+27 -19
View File
@@ -6,30 +6,42 @@ https://home-assistant.io/components/binary_sensor.rest/
"""
import logging
from homeassistant.components.binary_sensor import (BinarySensorDevice,
SENSOR_CLASSES)
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
from homeassistant.components.sensor.rest import RestData
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.const import (
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
CONF_SENSOR_CLASS)
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
DEFAULT_METHOD = 'GET'
DEFAULT_NAME = 'REST Binary Sensor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'REST Binary Sensor'
DEFAULT_METHOD = 'GET'
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the REST binary sensor."""
resource = config.get('resource', None)
method = config.get('method', DEFAULT_METHOD)
payload = config.get('payload', None)
name = config.get(CONF_NAME)
resource = config.get(CONF_RESOURCE)
method = config.get(CONF_METHOD)
payload = config.get(CONF_PAYLOAD)
verify_ssl = config.get('verify_ssl', True)
sensor_class = config.get('sensor_class')
if sensor_class not in SENSOR_CLASSES:
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
sensor_class = None
sensor_class = config.get(CONF_SENSOR_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
rest = RestData(method, resource, payload, verify_ssl)
rest.update()
@@ -39,11 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
add_devices([RestBinarySensor(
hass,
rest,
config.get('name', DEFAULT_NAME),
sensor_class,
config.get(CONF_VALUE_TEMPLATE))])
hass, rest, name, sensor_class, value_template)])
# pylint: disable=too-many-arguments
@@ -0,0 +1,9 @@
# Describes the format for available binary_sensor services
ffmpeg_restart:
description: Send a restart command to a ffmpeg based sensor (party mode).
fields:
entity_id:
description: Name(s) of entites that will restart. Platform dependent.
example: 'binary_sensor.ffmpeg_noise'
@@ -5,55 +5,47 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.template/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (BinarySensorDevice,
ENTITY_ID_FORMAT,
SENSOR_CLASSES)
from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE,
ATTR_ENTITY_ID, MATCH_ALL)
PLATFORM_SCHEMA,
SENSOR_CLASSES_SCHEMA)
from homeassistant.const import (ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, MATCH_ALL,
CONF_VALUE_TEMPLATE, CONF_SENSOR_CLASS)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers import template
from homeassistant.helpers.event import track_state_change
from homeassistant.util import slugify
CONF_SENSORS = 'sensors'
SENSOR_SCHEMA = vol.Schema({
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID, default=MATCH_ALL): cv.entity_ids,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
})
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup template binary sensors."""
sensors = []
if config.get(CONF_SENSORS) is None:
_LOGGER.error('Missing configuration data for binary_sensor platform')
return False
for device, device_config in config[CONF_SENSORS].items():
if device != slugify(device):
_LOGGER.error('Found invalid key for binary_sensor.template: %s. '
'Use %s instead', device, slugify(device))
continue
if not isinstance(device_config, dict):
_LOGGER.error('Missing configuration data for binary_sensor %s',
device)
continue
value_template = device_config[CONF_VALUE_TEMPLATE]
entity_ids = device_config[ATTR_ENTITY_ID]
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
sensor_class = device_config.get('sensor_class')
value_template = device_config.get(CONF_VALUE_TEMPLATE)
if sensor_class not in SENSOR_CLASSES:
_LOGGER.error('Sensor class is not valid')
continue
if value_template is None:
_LOGGER.error(
'Missing %s for sensor %s', CONF_VALUE_TEMPLATE, device)
continue
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
sensor_class = device_config.get(CONF_SENSOR_CLASS)
sensors.append(
BinarySensorTemplate(
@@ -6,9 +6,6 @@ https://home-assistant.io/components/binary_sensor.vera/
"""
import logging
import homeassistant.util.dt as dt_util
from homeassistant.const import (
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED)
from homeassistant.components.binary_sensor import (
BinarySensorDevice)
from homeassistant.components.vera import (
@@ -34,30 +31,6 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
self._state = False
VeraDevice.__init__(self, vera_device, controller)
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable:
armed = self.vera_device.is_armed
attr[ATTR_ARMED] = 'True' if armed else 'False'
if self.vera_device.is_trippable:
last_tripped = self.vera_device.last_trip
if last_tripped is not None:
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat()
else:
attr[ATTR_LAST_TRIP_TIME] = None
tripped = self.vera_device.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
@property
def is_on(self):
"""Return true if sensor is on."""
@@ -13,14 +13,15 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
REQUIREMENTS = ['python-wink==0.7.13', 'pubnub==3.8.2']
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
"opened": "opening",
"brightness": "light",
"vibration": "vibration",
"loudness": "sound"
"loudness": "sound",
"liquid_detected": "moisture"
}
@@ -74,6 +75,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
return self.wink.vibration_boolean()
elif self.capability == "brightness":
return self.wink.brightness_boolean()
elif self.capability == "liquid_detected":
return self.wink.liquid_boolean()
else:
return self.wink.state()
@@ -94,7 +94,8 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_ha_state()
+2 -1
View File
@@ -13,7 +13,8 @@ ATTR_URL = 'url'
ATTR_URL_DEFAULT = 'https://www.google.com'
SERVICE_BROWSE_URL_SCHEMA = vol.Schema({
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url,
# pylint: disable=no-value-for-parameter
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(),
})
+6 -6
View File
@@ -11,15 +11,15 @@ import requests
from homeassistant.components.camera import Camera
from homeassistant.loader import get_component
DEPENDENCIES = ["bloomsky"]
DEPENDENCIES = ['bloomsky']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup access to BloomSky cameras."""
bloomsky = get_component('bloomsky')
for device in bloomsky.BLOOMSKY.devices.values():
add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
class BloomSkyCamera(Camera):
@@ -28,8 +28,8 @@ class BloomSkyCamera(Camera):
def __init__(self, bs, device):
"""Setup for access to the BloomSky camera images."""
super(BloomSkyCamera, self).__init__()
self._name = device["DeviceName"]
self._id = device["DeviceID"]
self._name = device['DeviceName']
self._id = device['DeviceID']
self._bloomsky = bs
self._url = ""
self._last_url = ""
@@ -42,7 +42,7 @@ class BloomSkyCamera(Camera):
def camera_image(self):
"""Update the camera's image if it has changed."""
try:
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]
self._url = self._bloomsky.devices[self._id]['Data']['ImageURL']
self._bloomsky.refresh_devices()
# If the URL hasn't changed then the image hasn't changed.
if self._url != self._last_url:
+77
View File
@@ -0,0 +1,77 @@
"""
Support for Cameras with FFmpeg as decoder.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.ffmpeg/
"""
import logging
from contextlib import closing
import voluptuous as vol
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
REQUIREMENTS = ['ha-ffmpeg==0.9']
_LOGGER = logging.getLogger(__name__)
CONF_INPUT = 'input'
CONF_FFMPEG_BIN = 'ffmpeg_bin'
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
DEFAULT_BINARY = 'ffmpeg'
DEFAULT_NAME = 'FFmpeg'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_INPUT): cv.string,
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a FFmpeg Camera."""
add_devices([FFmpegCamera(config)])
class FFmpegCamera(Camera):
"""An implementation of an FFmpeg camera."""
def __init__(self, config):
"""Initialize a FFmpeg camera."""
super().__init__()
self._name = config.get(CONF_NAME)
self._input = config.get(CONF_INPUT)
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
self._ffmpeg_bin = config.get(CONF_FFMPEG_BIN)
def _ffmpeg_stream(self):
"""Return a FFmpeg process object."""
from haffmpeg import CameraMjpeg
ffmpeg = CameraMjpeg(self._ffmpeg_bin)
ffmpeg.open_camera(self._input, extra_cmd=self._extra_arguments)
return ffmpeg
def camera_image(self):
"""Return a still image response from the camera."""
with closing(self._ffmpeg_stream()) as stream:
return extract_image_from_mjpeg(stream)
def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from the camera."""
stream = self._ffmpeg_stream()
return response(
stream,
mimetype='multipart/x-mixed-replace;boundary=ffserver',
direct_passthrough=True
)
@property
def name(self):
"""Return the name of this camera."""
return self._name
+26 -14
View File
@@ -7,21 +7,33 @@ https://home-assistant.io/components/camera.foscam/
import logging
import requests
import voluptuous as vol
from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.helpers import validate_config
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_IP = 'ip'
DEFAULT_NAME = 'Foscam Camera'
DEFAULT_PORT = 88
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a Foscam IP Camera."""
if not validate_config({DOMAIN: config},
{DOMAIN: ['username', 'password', 'ip']}, _LOGGER):
return None
add_devices_callback([FoscamCamera(config)])
add_devices([FoscamCamera(config)])
# pylint: disable=too-many-instance-attributes
@@ -32,16 +44,16 @@ class FoscamCamera(Camera):
"""Initialize a Foscam camera."""
super(FoscamCamera, self).__init__()
ip_address = device_info.get('ip')
port = device_info.get('port', 88)
ip_address = device_info.get(CONF_IP)
port = device_info.get(CONF_PORT)
self._base_url = 'http://' + ip_address + ':' + str(port) + '/'
self._username = device_info.get('username')
self._password = device_info.get('password')
self._base_url = 'http://{}:{}/'.format(ip_address, port)
self._username = device_info.get(CONF_USERNAME)
self._password = device_info.get(CONF_PASSWORD)
self._snap_picture_url = self._base_url \
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
+ self._username + '&pwd=' + self._password
self._name = device_info.get('name', 'Foscam Camera')
self._name = device_info.get(CONF_NAME)
_LOGGER.info('Using the following URL for %s: %s',
self._name, self._snap_picture_url)
+62 -29
View File
@@ -7,22 +7,38 @@ https://home-assistant.io/components/camera.generic/
import logging
import requests
from requests.auth import HTTPBasicAuth
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.helpers import validate_config
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers import config_validation as cv, template
_LOGGER = logging.getLogger(__name__)
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
CONF_STILL_IMAGE_URL = 'still_image_url'
DEFAULT_NAME = 'Generic Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STILL_IMAGE_URL): vol.Any(cv.url, cv.template),
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a generic IP Camera."""
if not validate_config({DOMAIN: config}, {DOMAIN: ['still_image_url']},
_LOGGER):
return None
add_devices_callback([GenericCamera(config)])
add_devices([GenericCamera(config)])
# pylint: disable=too-many-instance-attributes
@@ -32,30 +48,47 @@ class GenericCamera(Camera):
def __init__(self, device_info):
"""Initialize a generic camera."""
super().__init__()
self._name = device_info.get('name', 'Generic Camera')
self._username = device_info.get('username')
self._password = device_info.get('password')
self._still_image_url = device_info['still_image_url']
self._name = device_info.get(CONF_NAME)
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
username = device_info.get(CONF_USERNAME)
password = device_info.get(CONF_PASSWORD)
if username and password:
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION:
self._auth = HTTPDigestAuth(username, password)
else:
self._auth = HTTPBasicAuth(username, password)
else:
self._auth = None
self._last_url = None
self._last_image = None
def camera_image(self):
"""Return a still image response from the camera."""
if self._username and self._password:
try:
response = requests.get(
self._still_image_url,
auth=HTTPBasicAuth(self._username, self._password),
timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
else:
try:
response = requests.get(self._still_image_url, timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
try:
url = template.render(self.hass, self._still_image_url)
except TemplateError as err:
_LOGGER.error('Error parsing template %s: %s',
self._still_image_url, err)
return self._last_image
return response.content
if url == self._last_url and self._limit_refetch:
return self._last_image
kwargs = {'timeout': 10, 'auth': self._auth}
try:
response = requests.get(url, **kwargs)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
self._last_url = url
self._last_image = response.content
return self._last_image
@property
def name(self):
+26 -21
View File
@@ -1,50 +1,55 @@
"""Camera that loads a picture from a local file."""
"""
Camera that loads a picture from a local file.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.local_file/
"""
import logging
import os
from homeassistant.components.camera import Camera
import voluptuous as vol
from homeassistant.const import CONF_NAME
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_FILE_PATH = 'file_path'
DEFAULT_NAME = 'Local File'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FILE_PATH): cv.isfile,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Camera."""
# check for missing required configuration variable
if config.get("file_path") is None:
_LOGGER.error("Missing required variable: file_path")
return False
setup_config = (
{
"name": config.get("name", "Local File"),
"file_path": config.get("file_path")
}
)
file_path = config[CONF_FILE_PATH]
# check filepath given is readable
if not os.access(setup_config["file_path"], os.R_OK):
if not os.access(file_path, os.R_OK):
_LOGGER.error("file path is not readable")
return False
add_devices([
LocalFile(setup_config)
])
add_devices([LocalFile(config[CONF_NAME], file_path)])
class LocalFile(Camera):
"""Local camera."""
def __init__(self, device_info):
def __init__(self, name, file_path):
"""Initialize Local File Camera component."""
super().__init__()
self._name = device_info["name"]
self._config = device_info
self._name = name
self._file_path = file_path
def camera_image(self):
"""Return image response."""
with open(self._config["file_path"], 'rb') as file:
with open(self._file_path, 'rb') as file:
return file.read()
@property
+45 -28
View File
@@ -8,24 +8,48 @@ import logging
from contextlib import closing
import requests
from requests.auth import HTTPBasicAuth
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.helpers import validate_config
CONTENT_TYPE_HEADER = 'Content-Type'
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_MJPEG_URL = 'mjpeg_url'
CONTENT_TYPE_HEADER = 'Content-Type'
DEFAULT_NAME = 'Mjpeg Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MJPEG_URL): cv.url,
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a MJPEG IP Camera."""
if not validate_config({DOMAIN: config}, {DOMAIN: ['mjpeg_url']},
_LOGGER):
return None
add_devices([MjpegCamera(config)])
add_devices_callback([MjpegCamera(config)])
def extract_image_from_mjpeg(stream):
"""Take in a MJPEG stream object, return the jpg from it."""
data = b''
for chunk in stream:
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
jpg = data[jpg_start:jpg_end + 2]
return jpg
# pylint: disable=too-many-instance-attributes
@@ -35,36 +59,29 @@ class MjpegCamera(Camera):
def __init__(self, device_info):
"""Initialize a MJPEG camera."""
super().__init__()
self._name = device_info.get('name', 'Mjpeg Camera')
self._username = device_info.get('username')
self._password = device_info.get('password')
self._mjpeg_url = device_info['mjpeg_url']
self._name = device_info.get(CONF_NAME)
self._authentication = device_info.get(CONF_AUTHENTICATION)
self._username = device_info.get(CONF_USERNAME)
self._password = device_info.get(CONF_PASSWORD)
self._mjpeg_url = device_info[CONF_MJPEG_URL]
def camera_stream(self):
"""Return a MJPEG stream image response directly from the camera."""
if self._username and self._password:
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(self._username, self._password)
else:
auth = HTTPBasicAuth(self._username, self._password)
return requests.get(self._mjpeg_url,
auth=HTTPBasicAuth(self._username,
self._password),
auth=auth,
stream=True, timeout=10)
else:
return requests.get(self._mjpeg_url, stream=True, timeout=10)
def camera_image(self):
"""Return a still image response from the camera."""
def process_response(response):
"""Take in a response object, return the jpg from it."""
data = b''
for chunk in response.iter_content(1024):
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
jpg = data[jpg_start:jpg_end + 2]
return jpg
with closing(self.camera_stream()) as response:
return process_response(response)
return extract_image_from_mjpeg(response.iter_content(1024))
def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from the camera."""
+18 -9
View File
@@ -6,34 +6,43 @@ https://home-assistant.io/components/camera.netatmo/
"""
import logging
from datetime import timedelta
import requests
import voluptuous as vol
from homeassistant.util import Throttle
from homeassistant.components.camera import Camera
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.loader import get_component
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ["netatmo"]
DEPENDENCIES = ['netatmo']
_LOGGER = logging.getLogger(__name__)
CONF_HOME = 'home'
ATTR_CAMERAS = 'cameras'
CONF_CAMERAS = 'cameras'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME): cv.string,
vol.Optional(CONF_CAMERAS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup access to Netatmo Welcome cameras."""
netatmo = get_component('netatmo')
home = config.get(CONF_HOME, None)
home = config.get(CONF_HOME)
data = WelcomeData(netatmo.NETATMO_AUTH, home)
for camera_name in data.get_camera_names():
if ATTR_CAMERAS in config:
if camera_name not in config[ATTR_CAMERAS]:
if CONF_CAMERAS in config:
if camera_name not in config[CONF_CAMERAS]:
continue
add_devices_callback([WelcomeCamera(data, camera_name, home)])
add_devices([WelcomeCamera(data, camera_name, home)])
class WelcomeCamera(Camera):
+66 -30
View File
@@ -9,41 +9,77 @@ import subprocess
import logging
import shutil
from homeassistant.components.camera import Camera
import voluptuous as vol
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_HORIZONTAL_FLIP = 'horizontal_flip'
CONF_IMAGE_HEIGHT = 'image_height'
CONF_IMAGE_QUALITY = 'image_quality'
CONF_IMAGE_ROTATION = 'image_rotation'
CONF_IMAGE_WIDTH = 'image_width'
CONF_TIMELAPSE = 'timelapse'
CONF_VERTICAL_FLIP = 'vertical_flip'
DEFAULT_HORIZONTAL_FLIP = 0
DEFAULT_IMAGE_HEIGHT = 480
DEFAULT_IMAGE_QUALITIY = 7
DEFAULT_IMAGE_ROTATION = 0
DEFAULT_IMAGE_WIDTH = 640
DEFAULT_NAME = 'Raspberry Pi Camera'
DEFAULT_TIMELAPSE = 1000
DEFAULT_VERTICAL_FLIP = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FILE_PATH): cv.isfile,
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
vol.Coerce(int),
vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY):
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION):
vol.All(vol.Coerce(int), vol.Range(min=0, max=359)),
vol.Optional(CONF_IMAGE_WIDTH, default=DEFAULT_IMAGE_WIDTH):
vol.Coerce(int),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TIMELAPSE, default=1000): vol.Coerce(int),
vol.Optional(CONF_VERTICAL_FLIP, default=DEFAULT_VERTICAL_FLIP):
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Raspberry Camera."""
if shutil.which("raspistill") is None:
_LOGGER.error("Error: raspistill not found")
_LOGGER.error("'raspistill' was not found")
return False
setup_config = (
{
"name": config.get("name", "Raspberry Pi Camera"),
"image_width": int(config.get("image_width", "640")),
"image_height": int(config.get("image_height", "480")),
"image_quality": int(config.get("image_quality", "7")),
"image_rotation": int(config.get("image_rotation", "0")),
"timelapse": int(config.get("timelapse", "2000")),
"horizontal_flip": int(config.get("horizontal_flip", "0")),
"vertical_flip": int(config.get("vertical_flip", "0")),
"file_path": config.get("file_path",
os.path.join(os.path.dirname(__file__),
'image.jpg'))
CONF_NAME: config.get(CONF_NAME),
CONF_IMAGE_WIDTH: config.get(CONF_IMAGE_WIDTH),
CONF_IMAGE_HEIGHT: config.get(CONF_IMAGE_HEIGHT),
CONF_IMAGE_QUALITY: config.get(CONF_IMAGE_QUALITY),
CONF_IMAGE_ROTATION: config.get(CONF_IMAGE_ROTATION),
CONF_TIMELAPSE: config.get(CONF_TIMELAPSE),
CONF_HORIZONTAL_FLIP: config.get(CONF_HORIZONTAL_FLIP),
CONF_VERTICAL_FLIP: config.get(CONF_VERTICAL_FLIP),
CONF_FILE_PATH: config.get(CONF_FILE_PATH,
os.path.join(os.path.dirname(__file__),
'image.jpg'))
}
)
# check filepath given is writable
if not os.access(setup_config["file_path"], os.W_OK):
_LOGGER.error("Error: file path is not writable")
if not os.access(setup_config[CONF_FILE_PATH], os.W_OK):
_LOGGER.error("File path is not writable")
return False
add_devices([
RaspberryCamera(setup_config)
])
add_devices([RaspberryCamera(setup_config)])
class RaspberryCamera(Camera):
@@ -53,26 +89,26 @@ class RaspberryCamera(Camera):
"""Initialize Raspberry Pi camera component."""
super().__init__()
self._name = device_info["name"]
self._name = device_info[CONF_NAME]
self._config = device_info
# kill if there's raspistill instance
# Kill if there's raspistill instance
subprocess.Popen(['killall', 'raspistill'],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
cmd_args = [
'raspistill', '--nopreview', '-o', str(device_info["file_path"]),
'-t', '0', '-w', str(device_info["image_width"]),
'-h', str(device_info["image_height"]),
'-tl', str(device_info["timelapse"]),
'-q', str(device_info["image_quality"]),
'-rot', str(device_info["image_rotation"])
'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],
'-t', '0', '-w', str(device_info[CONF_IMAGE_WIDTH]),
'-h', str(device_info[CONF_IMAGE_HEIGHT]),
'-tl', str(device_info[CONF_TIMELAPSE]),
'-q', str(device_info[CONF_IMAGE_QUALITY]),
'-rot', str(device_info[CONF_IMAGE_ROTATION])
]
if device_info["horizontal_flip"]:
if device_info[CONF_HORIZONTAL_FLIP]:
cmd_args.append("-hf")
if device_info["vertical_flip"]:
if device_info[CONF_VERTICAL_FLIP]:
cmd_args.append("-vf")
subprocess.Popen(cmd_args,
@@ -81,7 +117,7 @@ class RaspberryCamera(Camera):
def camera_image(self):
"""Return raspstill image response."""
with open(self._config["file_path"], 'rb') as file:
with open(self._config[CONF_FILE_PATH], 'rb') as file:
return file.read()
@property
@@ -0,0 +1,535 @@
"""
Provides functionality to interact with climate devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/climate/
"""
import logging
import os
from numbers import Number
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.config import load_yaml_config_file
import homeassistant.util as util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELSIUS)
DOMAIN = "climate"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = 60
SERVICE_SET_AWAY_MODE = "set_away_mode"
SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_OPERATION_MODE = "set_operation_mode"
SERVICE_SET_SWING_MODE = "set_swing_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
STATE_HEAT = "heat"
STATE_COOL = "cool"
STATE_IDLE = "idle"
STATE_AUTO = "auto"
STATE_DRY = "dry"
STATE_FAN_ONLY = "fan_only"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_temp"
ATTR_AWAY_MODE = "away_mode"
ATTR_AUX_HEAT = "aux_heat"
ATTR_FAN_MODE = "fan_mode"
ATTR_FAN_LIST = "fan_list"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_OPERATION_MODE = "operation_mode"
ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list"
_LOGGER = logging.getLogger(__name__)
SET_AWAY_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AWAY_MODE): cv.boolean,
})
SET_AUX_HEAT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AUX_HEAT): cv.boolean,
})
SET_TEMPERATURE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_TEMPERATURE): vol.Coerce(float),
})
SET_FAN_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MODE): cv.string,
})
SET_OPERATION_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_OPERATION_MODE): cv.string,
})
SET_HUMIDITY_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_HUMIDITY): vol.Coerce(float),
})
SET_SWING_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_SWING_MODE): cv.string,
})
def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified climate devices away mode on."""
data = {
ATTR_AWAY_MODE: away_mode
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
def set_aux_heat(hass, aux_heat, entity_id=None):
"""Turn all or specified climate devices auxillary heater on."""
data = {
ATTR_AUX_HEAT: aux_heat
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
def set_temperature(hass, temperature, entity_id=None):
"""Set new target temperature."""
data = {ATTR_TEMPERATURE: temperature}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data)
def set_humidity(hass, humidity, entity_id=None):
"""Set new target humidity."""
data = {ATTR_HUMIDITY: humidity}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data)
def set_fan_mode(hass, fan, entity_id=None):
"""Set all or specified climate devices fan mode on."""
data = {ATTR_FAN_MODE: fan}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode."""
data = {ATTR_OPERATION_MODE: operation_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
def set_swing_mode(hass, swing_mode, entity_id=None):
"""Set new target swing mode."""
data = {ATTR_SWING_MODE: swing_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
# pylint: disable=too-many-branches
def setup(hass, config):
"""Setup climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def away_mode_set_service(service):
"""Set away mode on target climate devices."""
target_climate = component.extract_from_service(service)
away_mode = service.data.get(ATTR_AWAY_MODE)
if away_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE)
return
for climate in target_climate:
if away_mode:
climate.turn_away_mode_on()
else:
climate.turn_away_mode_off()
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service,
descriptions.get(SERVICE_SET_AWAY_MODE),
schema=SET_AWAY_MODE_SCHEMA)
def aux_heat_set_service(service):
"""Set auxillary heater on target climate devices."""
target_climate = component.extract_from_service(service)
aux_heat = service.data.get(ATTR_AUX_HEAT)
if aux_heat is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT)
return
for climate in target_climate:
if aux_heat:
climate.turn_aux_heat_on()
else:
climate.turn_aux_heat_off()
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service,
descriptions.get(SERVICE_SET_AUX_HEAT),
schema=SET_AUX_HEAT_SCHEMA)
def temperature_set_service(service):
"""Set temperature on the target climate devices."""
target_climate = component.extract_from_service(service)
temperature = util.convert(
service.data.get(ATTR_TEMPERATURE), float)
if temperature is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE)
return
for climate in target_climate:
climate.set_temperature(convert_temperature(
temperature, hass.config.units.temperature_unit,
climate.unit_of_measurement))
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service,
descriptions.get(SERVICE_SET_TEMPERATURE),
schema=SET_TEMPERATURE_SCHEMA)
def humidity_set_service(service):
"""Set humidity on the target climate devices."""
target_climate = component.extract_from_service(service)
humidity = service.data.get(ATTR_HUMIDITY)
if humidity is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_HUMIDITY, ATTR_HUMIDITY)
return
for climate in target_climate:
climate.set_humidity(humidity)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service,
descriptions.get(SERVICE_SET_HUMIDITY),
schema=SET_HUMIDITY_SCHEMA)
def fan_mode_set_service(service):
"""Set fan mode on target climate devices."""
target_climate = component.extract_from_service(service)
fan = service.data.get(ATTR_FAN_MODE)
if fan is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
return
for climate in target_climate:
climate.set_fan_mode(fan)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service,
descriptions.get(SERVICE_SET_FAN_MODE),
schema=SET_FAN_MODE_SCHEMA)
def operation_set_service(service):
"""Set operating mode on the target climate devices."""
target_climate = component.extract_from_service(service)
operation_mode = service.data.get(ATTR_OPERATION_MODE)
if operation_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
return
for climate in target_climate:
climate.set_operation_mode(operation_mode)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service,
descriptions.get(SERVICE_SET_OPERATION_MODE),
schema=SET_OPERATION_MODE_SCHEMA)
def swing_set_service(service):
"""Set swing mode on the target climate devices."""
target_climate = component.extract_from_service(service)
swing_mode = service.data.get(ATTR_SWING_MODE)
if swing_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
return
for climate in target_climate:
climate.set_swing_mode(swing_mode)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
descriptions.get(SERVICE_SET_SWING_MODE),
schema=SET_SWING_MODE_SCHEMA)
return True
class ClimateDevice(Entity):
"""Representation of a climate device."""
# pylint: disable=too-many-public-methods,no-self-use
@property
def state(self):
"""Return the current state."""
return self.target_temperature or STATE_UNKNOWN
@property
def state_attributes(self):
"""Return the optional state attributes."""
data = {
ATTR_CURRENT_TEMPERATURE:
self._convert_for_display(self.current_temperature),
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE:
self._convert_for_display(self.target_temperature),
}
humidity = self.target_humidity
if humidity is not None:
data[ATTR_HUMIDITY] = humidity
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
data[ATTR_MIN_HUMIDITY] = self.min_humidity
data[ATTR_MAX_HUMIDITY] = self.max_humidity
fan_mode = self.current_fan_mode
if fan_mode is not None:
data[ATTR_FAN_MODE] = fan_mode
data[ATTR_FAN_LIST] = self.fan_list
operation_mode = self.current_operation
if operation_mode is not None:
data[ATTR_OPERATION_MODE] = operation_mode
data[ATTR_OPERATION_LIST] = self.operation_list
swing_mode = self.current_swing_mode
if swing_mode is not None:
data[ATTR_SWING_MODE] = swing_mode
data[ATTR_SWING_LIST] = self.swing_list
is_away = self.is_away_mode_on
if is_away is not None:
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
is_aux_heat = self.is_aux_heat_on
if is_aux_heat is not None:
data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF
return data
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
raise NotImplementedError
@property
def current_humidity(self):
"""Return the current humidity."""
return None
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return None
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return None
@property
def operation_list(self):
"""List of available operation modes."""
return None
@property
def current_temperature(self):
"""Return the current temperature."""
return None
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return None
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return None
@property
def is_aux_heat_on(self):
"""Return true if aux heater."""
return None
@property
def current_fan_mode(self):
"""Return the fan setting."""
return None
@property
def fan_list(self):
"""List of available fan modes."""
return None
@property
def current_swing_mode(self):
"""Return the fan setting."""
return None
@property
def swing_list(self):
"""List of available swing modes."""
return None
def set_temperature(self, temperature):
"""Set new target temperature."""
raise NotImplementedError()
def set_humidity(self, humidity):
"""Set new target humidity."""
raise NotImplementedError()
def set_fan_mode(self, fan):
"""Set new target fan mode."""
raise NotImplementedError()
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
raise NotImplementedError()
def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
raise NotImplementedError()
def turn_away_mode_on(self):
"""Turn away mode on."""
raise NotImplementedError()
def turn_away_mode_off(self):
"""Turn away mode off."""
raise NotImplementedError()
def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
raise NotImplementedError()
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
raise NotImplementedError()
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert_temperature(7, TEMP_CELSIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert_temperature(35, TEMP_CELSIUS, self.unit_of_measurement)
@property
def min_humidity(self):
"""Return the minimum humidity."""
return 30
@property
def max_humidity(self):
"""Return the maximum humidity."""
return 99
def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes."""
if temp is None or not isinstance(temp, Number):
return temp
value = convert_temperature(temp, self.unit_of_measurement,
self.hass.config.units.temperature_unit)
if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
decimal_count = 1
else:
# Users of fahrenheit generally expect integer units.
decimal_count = 0
return round(value, decimal_count)
+164
View File
@@ -0,0 +1,164 @@
"""
Demo platform that offers a fake climate device.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo climate devices."""
add_devices([
DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low",
None, None, "Auto", "Heat", None),
DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High",
67, 54, "Off", "Cool", False),
])
# pylint: disable=too-many-arguments, too-many-public-methods
class DemoClimate(ClimateDevice):
"""Representation of a demo climate device."""
# pylint: disable=too-many-instance-attributes
def __init__(self, name, target_temperature, unit_of_measurement,
away, current_temperature, current_fan_mode,
target_humidity, current_humidity, current_swing_mode,
current_operation, aux):
"""Initialize the climate device."""
self._name = name
self._target_temperature = target_temperature
self._target_humidity = target_humidity
self._unit_of_measurement = unit_of_measurement
self._away = away
self._current_temperature = current_temperature
self._current_humidity = current_humidity
self._current_fan_mode = current_fan_mode
self._current_operation = current_operation
self._aux = aux
self._current_swing_mode = current_swing_mode
self._fan_list = ["On Low", "On High", "Auto Low", "Auto High", "Off"]
self._operation_list = ["Heat", "Cool", "Auto Changeover", "Off"]
self._swing_list = ["Auto", "1", "2", "3", "Off"]
@property
def should_poll(self):
"""Polling not needed for a demo climate device."""
return False
@property
def name(self):
"""Return the name of the climate device."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def current_humidity(self):
"""Return the current humidity."""
return self._current_humidity
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return self._target_humidity
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_operation
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self._away
@property
def is_aux_heat_on(self):
"""Return true if away mode is on."""
return self._aux
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self._current_fan_mode
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
def set_temperature(self, temperature):
"""Set new target temperature."""
self._target_temperature = temperature
self.update_ha_state()
def set_humidity(self, humidity):
"""Set new target temperature."""
self._target_humidity = humidity
self.update_ha_state()
def set_swing_mode(self, swing_mode):
"""Set new target temperature."""
self._current_swing_mode = swing_mode
self.update_ha_state()
def set_fan_mode(self, fan):
"""Set new target temperature."""
self._current_fan_mode = fan
self.update_ha_state()
def set_operation_mode(self, operation_mode):
"""Set new target temperature."""
self._current_operation = operation_mode
self.update_ha_state()
@property
def current_swing_mode(self):
"""Return the swing setting."""
return self._current_swing_mode
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
def turn_away_mode_on(self):
"""Turn away mode on."""
self._away = True
self.update_ha_state()
def turn_away_mode_off(self):
"""Turn away mode off."""
self._away = False
self.update_ha_state()
def turn_aux_heat_on(self):
"""Turn away auxillary heater on."""
self._aux = True
self.update_ha_state()
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
self._aux = False
self.update_ha_state()
+251
View File
@@ -0,0 +1,251 @@
"""
Platform for Ecobee Thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.ecobee/
"""
import logging
from os import path
import voluptuous as vol
from homeassistant.components import ecobee
from homeassistant.components.climate import (
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['ecobee']
_LOGGER = logging.getLogger(__name__)
ECOBEE_CONFIG_FILE = 'ecobee.conf'
_CONFIGURING = {}
ATTR_FAN_MIN_ON_TIME = "fan_min_on_time"
SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time"
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Ecobee Thermostat Platform."""
if discovery_info is None:
return
data = ecobee.NETWORK
hold_temp = discovery_info['hold_temp']
_LOGGER.info(
"Loading ecobee thermostat component with hold_temp set to %s",
hold_temp)
devices = [Thermostat(data, index, hold_temp)
for index in range(len(data.ecobee.thermostats))]
add_devices(devices)
def fan_min_on_time_set_service(service):
"""Set the minimum fan on time on the target thermostats."""
entity_id = service.data.get('entity_id')
if entity_id:
target_thermostats = [device for device in devices
if device.entity_id == entity_id]
else:
target_thermostats = devices
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
for thermostat in target_thermostats:
thermostat.set_fan_min_on_time(str(fan_min_on_time))
thermostat.update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service,
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
# pylint: disable=too-many-public-methods, abstract-method
class Thermostat(ClimateDevice):
"""A thermostat class for Ecobee."""
def __init__(self, data, thermostat_index, hold_temp):
"""Initialize the thermostat."""
self.data = data
self.thermostat_index = thermostat_index
self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index)
self._name = self.thermostat['name']
self.hold_temp = hold_temp
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
'heat', 'off']
def update(self):
"""Get the latest state from the thermostat."""
self.data.update()
self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index)
@property
def name(self):
"""Return the name of the Ecobee Thermostat."""
return self.thermostat['name']
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
return self.thermostat['runtime']['actualTemperature'] / 10
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if (self.operation_mode == 'heat' or
self.operation_mode == 'auxHeatOnly'):
return self.target_temperature_low
elif self.operation_mode == 'cool':
return self.target_temperature_high
else:
return (self.target_temperature_low +
self.target_temperature_high) / 2
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredHeat'] / 10)
@property
def target_temperature_high(self):
"""Return the upper bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredCool'] / 10)
@property
def desired_fan_mode(self):
"""Return the desired fan mode of operation."""
return self.thermostat['runtime']['desiredFanMode']
@property
def fan(self):
"""Return the current fan state."""
if 'fan' in self.thermostat['equipmentStatus']:
return STATE_ON
else:
return STATE_OFF
@property
def current_operation(self):
"""Return current operation."""
return self.operation_mode
@property
def operation_list(self):
"""Return the operation modes list."""
return self._operation_list
@property
def operation_mode(self):
"""Return current operation ie. heat, cool, idle."""
return self.thermostat['settings']['hvacMode']
@property
def mode(self):
"""Return current mode ie. home, away, sleep."""
return self.thermostat['program']['currentClimateRef']
@property
def fan_min_on_time(self):
"""Return current fan minimum on time."""
return self.thermostat['settings']['fanMinOnTime']
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
# Move these to Thermostat Device and make them global
status = self.thermostat['equipmentStatus']
operation = None
if status == '':
operation = STATE_IDLE
elif 'Cool' in status:
operation = STATE_COOL
elif 'auxHeat' in status:
operation = STATE_HEAT
elif 'heatPump' in status:
operation = STATE_HEAT
else:
operation = status
return {
"humidity": self.thermostat['runtime']['actualHumidity'],
"fan": self.fan,
"mode": self.mode,
"operation": operation,
"fan_min_on_time": self.fan_min_on_time
}
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
mode = self.mode
events = self.thermostat['events']
for event in events:
if event['running']:
mode = event['holdClimateRef']
break
return 'away' in mode
def turn_away_mode_on(self):
"""Turn away on."""
if self.hold_temp:
self.data.ecobee.set_climate_hold(self.thermostat_index,
"away", "indefinite")
else:
self.data.ecobee.set_climate_hold(self.thermostat_index, "away")
def turn_away_mode_off(self):
"""Turn away off."""
self.data.ecobee.resume_program(self.thermostat_index)
def set_temperature(self, temperature):
"""Set new target temperature."""
temperature = int(temperature)
low_temp = temperature - 1
high_temp = temperature + 1
if self.hold_temp:
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
high_temp, "indefinite")
else:
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
high_temp)
def set_operation_mode(self, operation_mode):
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode)
def set_fan_min_on_time(self, fan_min_on_time):
"""Set the minimum fan on time."""
self.data.ecobee.set_fan_min_on_time(self.thermostat_index,
fan_min_on_time)
# Home and Sleep mode aren't used in UI yet:
# def turn_home_mode_on(self):
# """ Turns home mode on. """
# self.data.ecobee.set_climate_hold(self.thermostat_index, "home")
# def turn_home_mode_off(self):
# """ Turns home mode off. """
# self.data.ecobee.resume_program(self.thermostat_index)
# def turn_sleep_mode_on(self):
# """ Turns sleep mode on. """
# self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep")
# def turn_sleep_mode_off(self):
# """ Turns sleep mode off. """
# self.data.ecobee.resume_program(self.thermostat_index)
@@ -0,0 +1,90 @@
"""
Support for eq3 Bluetooth Smart thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.eq3btsmart/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
from homeassistant.util.temperature import convert
REQUIREMENTS = ['bluepy_devices==0.2.0']
CONF_MAC = 'mac'
CONF_DEVICES = 'devices'
CONF_ID = 'id'
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the eq3 BLE thermostats."""
devices = []
for name, device_cfg in config[CONF_DEVICES].items():
mac = device_cfg[CONF_MAC]
devices.append(EQ3BTSmartThermostat(mac, name))
add_devices(devices)
return True
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
class EQ3BTSmartThermostat(ClimateDevice):
"""Representation of a EQ3 Bluetooth Smart thermostat."""
def __init__(self, _mac, _name):
"""Initialize the thermostat."""
from bluepy_devices.devices import eq3btsmart
self._name = _name
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Can not report temperature, so return target_temperature."""
return self.target_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._thermostat.target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
self._thermostat.target_temperature = temperature
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {"mode": self._thermostat.mode,
"mode_readable": self._thermostat.mode_readable}
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert(self._thermostat.min_temp, TEMP_CELSIUS,
self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert(self._thermostat.max_temp, TEMP_CELSIUS,
self.unit_of_measurement)
def update(self):
"""Update the data from the thermostat."""
self._thermostat.update()
@@ -0,0 +1,216 @@
"""
Adds support for generic thermostat units.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.generic_thermostat/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components import switch
from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF
from homeassistant.helpers import condition
from homeassistant.helpers.event import track_state_change
DEPENDENCIES = ['switch', 'sensor']
TOL_TEMP = 0.3
CONF_NAME = 'name'
DEFAULT_NAME = 'Generic Thermostat'
CONF_HEATER = 'heater'
CONF_SENSOR = 'target_sensor'
CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp'
CONF_TARGET_TEMP = 'target_temp'
CONF_AC_MODE = 'ac_mode'
CONF_MIN_DUR = 'min_cycle_duration'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): "generic_thermostat",
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HEATER): cv.entity_id,
vol.Required(CONF_SENSOR): cv.entity_id,
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
vol.Optional(CONF_AC_MODE): vol.Coerce(bool),
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the generic thermostat."""
name = config.get(CONF_NAME)
heater_entity_id = config.get(CONF_HEATER)
sensor_entity_id = config.get(CONF_SENSOR)
min_temp = config.get(CONF_MIN_TEMP)
max_temp = config.get(CONF_MAX_TEMP)
target_temp = config.get(CONF_TARGET_TEMP)
ac_mode = config.get(CONF_AC_MODE)
min_cycle_duration = config.get(CONF_MIN_DUR)
add_devices([GenericThermostat(hass, name, heater_entity_id,
sensor_entity_id, min_temp,
max_temp, target_temp, ac_mode,
min_cycle_duration)])
# pylint: disable=too-many-instance-attributes, abstract-method
class GenericThermostat(ClimateDevice):
"""Representation of a GenericThermostat device."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
"""Initialize the thermostat."""
self.hass = hass
self._name = name
self.heater_entity_id = heater_entity_id
self.ac_mode = ac_mode
self.min_cycle_duration = min_cycle_duration
self._active = False
self._cur_temp = None
self._min_temp = min_temp
self._max_temp = max_temp
self._target_temp = target_temp
self._unit = hass.config.units.temperature_unit
track_state_change(hass, sensor_entity_id, self._sensor_changed)
sensor_state = hass.states.get(sensor_entity_id)
if sensor_state:
self._update_temp(sensor_state)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the thermostat."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit
@property
def current_temperature(self):
"""Return the sensor temperature."""
return self._cur_temp
@property
def operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.ac_mode:
cooling = self._active and self._is_device_active
return STATE_COOL if cooling else STATE_IDLE
else:
heating = self._active and self._is_device_active
return STATE_HEAT if heating else STATE_IDLE
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temp
def set_temperature(self, temperature):
"""Set new target temperature."""
self._target_temp = temperature
self._control_heating()
self.update_ha_state()
@property
def min_temp(self):
"""Return the minimum temperature."""
# pylint: disable=no-member
if self._min_temp:
return self._min_temp
else:
# get default temp from super class
return ClimateDevice.min_temp.fget(self)
@property
def max_temp(self):
"""Return the maximum temperature."""
# pylint: disable=no-member
if self._min_temp:
return self._max_temp
else:
# Get default temp from super class
return ClimateDevice.max_temp.fget(self)
def _sensor_changed(self, entity_id, old_state, new_state):
"""Called when temperature changes."""
if new_state is None:
return
self._update_temp(new_state)
self._control_heating()
self.update_ha_state()
def _update_temp(self, state):
"""Update thermostat with latest state from sensor."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
try:
self._cur_temp = self.hass.config.units.temperature(
float(state.state), unit)
except ValueError as ex:
_LOGGER.error('Unable to update from sensor: %s', ex)
def _control_heating(self):
"""Check if we need to turn heating on or off."""
if not self._active and None not in (self._cur_temp,
self._target_temp):
self._active = True
_LOGGER.info('Obtained current and target temperature. '
'Generic thermostat active.')
if not self._active:
return
if self.min_cycle_duration:
if self._is_device_active:
current_state = STATE_ON
else:
current_state = STATE_OFF
long_enough = condition.state(self.hass, self.heater_entity_id,
current_state,
self.min_cycle_duration)
if not long_enough:
return
if self.ac_mode:
too_hot = self._cur_temp - self._target_temp > TOL_TEMP
is_cooling = self._is_device_active
if too_hot and not is_cooling:
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id)
elif not too_hot and is_cooling:
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id)
else:
too_cold = self._target_temp - self._cur_temp > TOL_TEMP
is_heating = self._is_device_active
if too_cold and not is_heating:
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id)
elif not too_cold and is_heating:
_LOGGER.info('Turning off heater %s', self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id)
@property
def _is_device_active(self):
"""If the toggleable device is currently active."""
return switch.is_on(self.hass, self.heater_entity_id)
@@ -0,0 +1,114 @@
"""
Support for the PRT Heatmiser themostats using the V3 protocol.
See https://github.com/andylockran/heatmiserV3 for more info on the
heatmiserV3 module dependency.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.heatmiser/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
CONF_IPADDRESS = 'ipaddress'
CONF_PORT = 'port'
CONF_TSTATS = 'tstats'
REQUIREMENTS = ["heatmiserV3==0.9.1"]
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the heatmiser thermostat."""
from heatmiserV3 import heatmiser, connection
ipaddress = str(config[CONF_IPADDRESS])
port = str(config[CONF_PORT])
if ipaddress is None or port is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_IPADDRESS, CONF_PORT)
return False
serport = connection.connection(ipaddress, port)
serport.open()
tstats = []
if CONF_TSTATS in config:
tstats = config[CONF_TSTATS]
if tstats is None:
_LOGGER.error("No thermostats configured.")
return False
for tstat in tstats:
add_devices([
HeatmiserV3Thermostat(
heatmiser,
tstat.get("id"),
tstat.get("name"),
serport)
])
return
class HeatmiserV3Thermostat(ClimateDevice):
"""Representation of a HeatmiserV3 thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, heatmiser, device, name, serport):
"""Initialize the thermostat."""
self.heatmiser = heatmiser
self.device = device
self.serport = serport
self._current_temperature = None
self._name = name
self._id = device
self.dcb = None
self.update()
self._target_temperature = int(self.dcb.get("roomset"))
@property
def name(self):
"""Return the name of the thermostat, if any."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if self.dcb is not None:
low = self.dcb.get("floortemplow ")
high = self.dcb.get("floortemphigh")
temp = (high*256 + low)/10.0
self._current_temperature = temp
else:
self._current_temperature = None
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
temperature = int(temperature)
self.heatmiser.hmSendAddress(
self._id,
18,
temperature,
1,
self.serport)
self._target_temperature = int(temperature)
def update(self):
"""Get the latest data."""
self.dcb = self.heatmiser.hmReadAddress(self._id, 'prt', self.serport)
@@ -0,0 +1,143 @@
"""
Support for Homematic thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematic/
"""
import logging
import homeassistant.components.homematic as homematic
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
from homeassistant.util.temperature import convert
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN
DEPENDENCIES = ['homematic']
STATE_MANUAL = "manual"
STATE_BOOST = "boost"
HM_STATE_MAP = {
"AUTO_MODE": STATE_AUTO,
"MANU_MODE": STATE_MANUAL,
"BOOST_MODE": STATE_BOOST,
}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the Homematic thermostat platform."""
if discovery_info is None:
return
return homematic.setup_hmdevice_discovery_helper(HMThermostat,
discovery_info,
add_callback_devices)
# pylint: disable=abstract-method
class HMThermostat(homematic.HMDevice, ClimateDevice):
"""Representation of a Homematic thermostat."""
@property
def unit_of_measurement(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
if not self.available:
return None
# read state and search
for mode, state in HM_STATE_MAP.items():
code = getattr(self._hmdevice, mode, 0)
if self._data.get('CONTROL_MODE') == code:
return state
@property
def operation_list(self):
"""List of available operation modes."""
if not self.available:
return None
op_list = []
# generate list
for mode in self._hmdevice.ACTIONNODE:
if mode in HM_STATE_MAP:
op_list.append(HM_STATE_MAP.get(mode))
return op_list
@property
def current_humidity(self):
"""Return the current humidity."""
if not self.available:
return None
return self._data.get('ACTUAL_HUMIDITY', None)
@property
def current_temperature(self):
"""Return the current temperature."""
if not self.available:
return None
return self._data.get('ACTUAL_TEMPERATURE', None)
@property
def target_temperature(self):
"""Return the target temperature."""
if not self.available:
return None
return self._data.get('SET_TEMPERATURE', None)
def set_temperature(self, temperature):
"""Set new target temperature."""
if not self.available:
return None
self._hmdevice.set_temperature(temperature)
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
for mode, state in HM_STATE_MAP.items():
if state == operation_mode:
code = getattr(self._hmdevice, mode, 0)
self._hmdevice.STATE = code
@property
def min_temp(self):
"""Return the minimum temperature - 4.5 means off."""
return convert(4.5, TEMP_CELSIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature - 30.5 means on."""
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
def _check_hm_to_ha_object(self):
"""Check if possible to use the Homematic object as this HA type."""
from pyhomematic.devicetypes.thermostats import HMThermostat\
as pyHMThermostat
# 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 isinstance(self._hmdevice, pyHMThermostat):
return True
_LOGGER.critical("This %s can't be use as thermostat", self._name)
return False
def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata."""
super()._init_data_struct()
# Add state to data dict
self._data.update({"CONTROL_MODE": STATE_UNKNOWN,
"SET_TEMPERATURE": STATE_UNKNOWN,
"ACTUAL_TEMPERATURE": STATE_UNKNOWN})
# support humidity
if 'ACTUAL_HUMIDITY' in self._hmdevice.SENSORNODE:
self._data.update({'ACTUAL_HUMIDITY': STATE_UNKNOWN})
@@ -0,0 +1,266 @@
"""
Support for Honeywell Round Connected and Honeywell Evohome thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.honeywell/
"""
import logging
import socket
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['evohomeclient==0.2.5',
'somecomfort==0.2.1']
_LOGGER = logging.getLogger(__name__)
CONF_AWAY_TEMP = "away_temperature"
DEFAULT_AWAY_TEMP = 16
def _setup_round(username, password, config, add_devices):
"""Setup rounding function."""
from evohomeclient import EvohomeClient
try:
away_temp = float(config.get(CONF_AWAY_TEMP, DEFAULT_AWAY_TEMP))
except ValueError:
_LOGGER.error("value entered for item %s should convert to a number",
CONF_AWAY_TEMP)
return False
evo_api = EvohomeClient(username, password)
try:
zones = evo_api.temperatures(force_refresh=True)
for i, zone in enumerate(zones):
add_devices([RoundThermostat(evo_api,
zone['id'],
i == 0,
away_temp)])
except socket.error:
_LOGGER.error(
"Connection error logging into the honeywell evohome web service"
)
return False
return True
# config will be used later
def _setup_us(username, password, config, add_devices):
"""Setup user."""
import somecomfort
try:
client = somecomfort.SomeComfort(username, password)
except somecomfort.AuthError:
_LOGGER.error('Failed to login to honeywell account %s', username)
return False
except somecomfort.SomeComfortError as ex:
_LOGGER.error('Failed to initialize honeywell client: %s', str(ex))
return False
dev_id = config.get('thermostat')
loc_id = config.get('location')
add_devices([HoneywellUSThermostat(client, device)
for location in client.locations_by_id.values()
for device in location.devices_by_id.values()
if ((not loc_id or location.locationid == loc_id) and
(not dev_id or device.deviceid == dev_id))])
return True
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the honeywel thermostat."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
region = config.get('region', 'eu').lower()
if username is None or password is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_USERNAME, CONF_PASSWORD)
return False
if region not in ('us', 'eu'):
_LOGGER.error('Region `%s` is invalid (use either us or eu)', region)
return False
if region == 'us':
return _setup_us(username, password, config, add_devices)
else:
return _setup_round(username, password, config, add_devices)
class RoundThermostat(ClimateDevice):
"""Representation of a Honeywell Round Connected thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, device, zone_id, master, away_temp):
"""Initialize the thermostat."""
self.device = device
self._current_temperature = None
self._target_temperature = None
self._name = "round connected"
self._id = zone_id
self._master = master
self._is_dhw = False
self._away_temp = away_temp
self._away = False
self.update()
@property
def name(self):
"""Return the name of the honeywell, if any."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self._is_dhw:
return None
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
self.device.set_temperature(self._name, temperature)
@property
def current_operation(self: ClimateDevice) -> str:
"""Get the current operation of the system."""
return getattr(self.device, 'system_mode', None)
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
"""Set the HVAC mode for the thermostat."""
if hasattr(self.device, 'system_mode'):
self.device.system_mode = operation_mode
def turn_away_mode_on(self):
"""Turn away on.
Evohome does have a proprietary away mode, but it doesn't really work
the way it should. For example: If you set a temperature manually
it doesn't get overwritten when away mode is switched on.
"""
self._away = True
self.device.set_temperature(self._name, self._away_temp)
def turn_away_mode_off(self):
"""Turn away off."""
self._away = False
self.device.cancel_temp_override(self._name)
def update(self):
"""Get the latest date."""
try:
# Only refresh if this is the "master" device,
# others will pick up the cache
for val in self.device.temperatures(force_refresh=self._master):
if val['id'] == self._id:
data = val
except StopIteration:
_LOGGER.error("Did not receive any temperature data from the "
"evohomeclient API.")
return
self._current_temperature = data['temp']
self._target_temperature = data['setpoint']
if data['thermostat'] == "DOMESTIC_HOT_WATER":
self._name = "Hot Water"
self._is_dhw = True
else:
self._name = data['name']
self._is_dhw = False
# pylint: disable=abstract-method
class HoneywellUSThermostat(ClimateDevice):
"""Representation of a Honeywell US Thermostat."""
def __init__(self, client, device):
"""Initialize the thermostat."""
self._client = client
self._device = device
@property
def is_fan_on(self):
"""Return true if fan is on."""
return self._device.fan_running
@property
def name(self):
"""Return the name of the honeywell, if any."""
return self._device.name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return (TEMP_CELSIUS if self._device.temperature_unit == 'C'
else TEMP_FAHRENHEIT)
@property
def current_temperature(self):
"""Return the current temperature."""
self._device.refresh()
return self._device.current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self._device.system_mode == 'cool':
return self._device.setpoint_cool
else:
return self._device.setpoint_heat
@property
def current_operation(self: ClimateDevice) -> str:
"""Return current operation ie. heat, cool, idle."""
return getattr(self._device, 'system_mode', None)
def set_temperature(self, temperature):
"""Set target temperature."""
import somecomfort
try:
if self._device.system_mode == 'cool':
self._device.setpoint_cool = temperature
else:
self._device.setpoint_heat = temperature
except somecomfort.SomeComfortError:
_LOGGER.error('Temperature %.1f out of range', temperature)
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {'fan': (self.is_fan_on and 'running' or 'idle'),
'fanmode': self._device.fan_mode,
'system_mode': self._device.system_mode}
def turn_away_mode_on(self):
"""Turn away on."""
pass
def turn_away_mode_off(self):
"""Turn away off."""
pass
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
"""Set the system mode (Cool, Heat, etc)."""
if hasattr(self._device, 'system_mode'):
self._device.system_mode = operation_mode
+83
View File
@@ -0,0 +1,83 @@
"""
Support for KNX thermostats.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/knx/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.knx import (
KNXConfig, KNXMultiAddressDevice)
DEPENDENCIES = ["knx"]
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create and add an entity based on the configuration."""
add_entities([
KNXThermostat(hass, KNXConfig(config))
])
class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
"""Representation of a KNX thermostat.
A KNX thermostat will has the following parameters:
- temperature (current temperature)
- setpoint (target temperature in HASS terms)
- operation mode selection (comfort/night/frost protection)
This version supports only polling. Messages from the KNX bus do not
automatically update the state of the thermostat (to be implemented
in future releases)
"""
def __init__(self, hass, config):
"""Initialize the thermostat based on the given configuration."""
KNXMultiAddressDevice.__init__(self, hass, config,
["temperature", "setpoint"],
["mode"])
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
self._away = False # not yet supported
self._is_fan_on = False # not yet supported
@property
def should_poll(self):
"""Polling is needed for the KNX thermostat."""
return True
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value("temperature"))
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value("setpoint"))
def set_temperature(self, temperature):
"""Set new target temperature."""
from knxip.conversion import float_to_knx2
self.set_value("setpoint", float_to_knx2(temperature))
_LOGGER.debug("Set target temperature to %s", temperature)
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
raise NotImplementedError()
+189
View File
@@ -0,0 +1,189 @@
"""
Support for Nest thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.nest/
"""
import voluptuous as vol
import homeassistant.components.nest as nest
from homeassistant.components.climate import (
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import TEMP_CELSIUS, CONF_PLATFORM, CONF_SCAN_INTERVAL
DEPENDENCIES = ['nest']
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): nest.DOMAIN,
vol.Optional(CONF_SCAN_INTERVAL):
vol.All(vol.Coerce(int), vol.Range(min=1)),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest thermostat."""
add_devices([NestThermostat(structure, device)
for structure, device in nest.devices()])
# pylint: disable=abstract-method
class NestThermostat(ClimateDevice):
"""Representation of a Nest thermostat."""
def __init__(self, structure, device):
"""Initialize the thermostat."""
self.structure = structure
self.device = device
@property
def name(self):
"""Return the name of the nest, if any."""
location = self.device.where
name = self.device.name
if location is None:
return name
else:
if name == '':
return location.capitalize()
else:
return location.capitalize() + '(' + name + ')'
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
# Move these to Thermostat Device and make them global
return {
"humidity": self.device.humidity,
"target_humidity": self.device.target_humidity,
"mode": self.device.mode
}
@property
def current_temperature(self):
"""Return the current temperature."""
return self.device.temperature
@property
def operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.device.hvac_ac_state is True:
return STATE_COOL
elif self.device.hvac_heater_state is True:
return STATE_HEAT
else:
return STATE_IDLE
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self.device.mode == 'range':
low, high = self.target_temperature_low, \
self.target_temperature_high
if self.operation == STATE_COOL:
temp = high
elif self.operation == STATE_HEAT:
temp = low
else:
# If the outside temp is lower than the current temp, consider
# the 'low' temp to the target, otherwise use the high temp
if (self.device.structure.weather.current.temperature <
self.current_temperature):
temp = low
else:
temp = high
else:
if self.is_away_mode_on:
# away_temperature is a low, high tuple. Only one should be set
# if not in range mode, the other will be None
temp = self.device.away_temperature[0] or \
self.device.away_temperature[1]
else:
temp = self.device.target
return temp
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
if self.is_away_mode_on and self.device.away_temperature[0]:
# away_temperature is always a low, high tuple
return self.device.away_temperature[0]
if self.device.mode == 'range':
return self.device.target[0]
return self.target_temperature
@property
def target_temperature_high(self):
"""Return the upper bound temperature we try to reach."""
if self.is_away_mode_on and self.device.away_temperature[1]:
# away_temperature is always a low, high tuple
return self.device.away_temperature[1]
if self.device.mode == 'range':
return self.device.target[1]
return self.target_temperature
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self.structure.away
def set_temperature(self, temperature):
"""Set new target temperature."""
if self.device.mode == 'range':
if self.target_temperature == self.target_temperature_low:
temperature = (temperature, self.target_temperature_high)
elif self.target_temperature == self.target_temperature_high:
temperature = (self.target_temperature_low, temperature)
self.device.target = temperature
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
self.device.mode = operation_mode
def turn_away_mode_on(self):
"""Turn away on."""
self.structure.away = True
def turn_away_mode_off(self):
"""Turn away off."""
self.structure.away = False
@property
def is_fan_on(self):
"""Return whether the fan is on."""
return self.device.fan
def turn_fan_on(self):
"""Turn fan on."""
self.device.fan = True
def turn_fan_off(self):
"""Turn fan off."""
self.device.fan = False
@property
def min_temp(self):
"""Identify min_temp in Nest API or defaults if not available."""
temp = self.device.away_temperature.low
if temp is None:
return super().min_temp
else:
return temp
@property
def max_temp(self):
"""Identify max_temp in Nest API or defaults if not available."""
temp = self.device.away_temperature.high
if temp is None:
return super().max_temp
else:
return temp
def update(self):
"""Python-nest has its own mechanism for staying up to date."""
pass
@@ -0,0 +1,90 @@
"""
Support for Proliphix NT10e Thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.proliphix/
"""
from homeassistant.components.climate import (
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT)
REQUIREMENTS = ['proliphix==0.3.1']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Proliphix thermostats."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
import proliphix
pdp = proliphix.PDP(host, username, password)
add_devices([
ProliphixThermostat(pdp)
])
# pylint: disable=abstract-method
class ProliphixThermostat(ClimateDevice):
"""Representation a Proliphix thermostat."""
def __init__(self, pdp):
"""Initialize the thermostat."""
self._pdp = pdp
# initial data
self._pdp.update()
self._name = self._pdp.name
@property
def should_poll(self):
"""Polling needed for thermostat."""
return True
def update(self):
"""Update the data from the thermostat."""
self._pdp.update()
@property
def name(self):
"""Return the name of the thermostat."""
return self._name
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
"fan": self._pdp.fan_state
}
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
return self._pdp.cur_temp
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._pdp.setback
@property
def current_operation(self):
"""Return the current state of the thermostat."""
state = self._pdp.hvac_state
if state in (1, 2):
return STATE_IDLE
elif state == 3:
return STATE_HEAT
elif state == 6:
return STATE_COOL
def set_temperature(self, temperature):
"""Set new target temperature."""
self._pdp.setback = temperature
@@ -0,0 +1,136 @@
"""
Support for Radio Thermostat wifi-enabled home thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.radiotherm/
"""
import datetime
import logging
from urllib.error import URLError
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF,
ClimateDevice)
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT
REQUIREMENTS = ['radiotherm==1.2']
HOLD_TEMP = 'hold_temp'
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Radio Thermostat."""
import radiotherm
hosts = []
if CONF_HOST in config:
hosts = config[CONF_HOST]
else:
hosts.append(radiotherm.discover.discover_address())
if hosts is None:
_LOGGER.error("No radiotherm thermostats detected.")
return False
hold_temp = config.get(HOLD_TEMP, False)
tstats = []
for host in hosts:
try:
tstat = radiotherm.get_thermostat(host)
tstats.append(RadioThermostat(tstat, hold_temp))
except (URLError, OSError):
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
host)
add_devices(tstats)
# pylint: disable=abstract-method
class RadioThermostat(ClimateDevice):
"""Representation of a Radio Thermostat."""
def __init__(self, device, hold_temp):
"""Initialize the thermostat."""
self.device = device
self.set_time()
self._target_temperature = None
self._current_temperature = None
self._current_operation = STATE_IDLE
self._name = None
self.hold_temp = hold_temp
self.update()
@property
def name(self):
"""Return the name of the Radio Thermostat."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
"fan": self.device.fmode['human'],
"mode": self.device.tmode['human']
}
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def current_operation(self):
"""Return the current operation. head, cool idle."""
return self._current_operation
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def update(self):
"""Update the data from the thermostat."""
self._current_temperature = self.device.temp['raw']
self._name = self.device.name['raw']
if self.device.tmode['human'] == 'Cool':
self._target_temperature = self.device.t_cool['raw']
self._current_operation = STATE_COOL
elif self.device.tmode['human'] == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_HEAT
else:
self._current_operation = STATE_IDLE
def set_temperature(self, temperature):
"""Set new target temperature."""
if self._current_operation == STATE_COOL:
self.device.t_cool = temperature
elif self._current_operation == STATE_HEAT:
self.device.t_heat = temperature
if self.hold_temp:
self.device.hold = 1
else:
self.device.hold = 0
def set_time(self):
"""Set device time."""
now = datetime.datetime.now()
self.device.time = {'day': now.weekday(),
'hour': now.hour, 'minute': now.minute}
def set_operation_mode(self, operation_mode):
"""Set operation mode (auto, cool, heat, off)."""
if operation_mode == STATE_OFF:
self.device.tmode = 0
elif operation_mode == STATE_AUTO:
self.device.tmode = 3
elif operation_mode == STATE_COOL:
self.device.t_cool = self._target_temperature
elif operation_mode == STATE_HEAT:
self.device.t_heat = self._target_temperature
@@ -0,0 +1,84 @@
set_aux_heat:
description: Turn auxillary heater on/off for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
aux_heat:
description: New value of axillary heater
example: true
set_away_mode:
description: Turn away mode on/off for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
away_mode:
description: New value of away mode
example: true
set_temperature:
description: Set target temperature of climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
temperature:
description: New target temperature for hvac
example: 25
set_humidity:
description: Set target humidity of climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
humidity:
description: New target humidity for climate device
example: 60
set_fan_mode:
description: Set fan operation for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.nest'
fan:
description: New value of fan mode
example: On Low
set_operation_mode:
description: Set operation mode for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climet.nest'
operation_mode:
description: New value of operation mode
example: Heat
set_swing_mode:
description: Set swing operation for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: '.nest'
swing_mode:
description: New value of swing mode
example: 1
+266
View File
@@ -0,0 +1,266 @@
"""
Support for ZWave climate devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.climate import DOMAIN
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.zwave import (
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
from homeassistant.components import zwave
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
_LOGGER = logging.getLogger(__name__)
CONF_NAME = 'name'
DEFAULT_NAME = 'ZWave Climate'
REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
COMMAND_CLASS_CONFIGURATION = 0x70
WORKAROUND_ZXT_120 = 'zxt_120'
DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
}
SET_TEMP_TO_INDEX = {
'Heat': 1,
'Cool': 2,
'Auto': 3,
'Aux Heat': 4,
'Resume': 5,
'Fan Only': 6,
'Furnace': 7,
'Dry Air': 8,
'Moist Air': 9,
'Auto Changeover': 10,
'Heat Econ': 11,
'Cool Econ': 12,
'Away': 13,
'Unknown': 14
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ZWave Climate devices."""
if discovery_info is None or zwave.NETWORK is None:
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
discovery_info, zwave.NETWORK)
return
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
value = node.values[discovery_info[ATTR_VALUE_ID]]
value.set_change_verified(False)
add_devices([ZWaveClimate(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments, abstract-method
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Represents a ZWave Climate device."""
# pylint: disable=too-many-public-methods, too-many-instance-attributes
def __init__(self, value):
"""Initialize the zwave climate device."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
self._target_temperature = None
self._current_temperature = None
self._current_operation = None
self._operation_list = None
self._current_fan_mode = None
self._fan_list = None
self._current_swing_mode = None
self._swing_list = None
self._unit = None
self._zxt_120 = None
self.update_properties()
# register listener
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
# 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_ZXT_120:
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
" workaround")
self._zxt_120 = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Set point
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
self._unit = value.units
if self.current_operation is not None:
if SET_TEMP_TO_INDEX.get(self._current_operation) \
!= value.index:
continue
if self._zxt_120:
continue
self._target_temperature = int(value.data)
# Operation Mode
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
self._current_operation = value.data
self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list)
_LOGGER.debug("self._current_operation=%s",
self._current_operation)
# Current Temp
for value in self._node.get_values(
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
if value.label == 'Temperature':
self._current_temperature = int(value.data)
# Fan Mode
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
self._current_fan_mode = value.data
self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list)
_LOGGER.debug("self._current_fan_mode=%s",
self._current_fan_mode)
# Swing mode
if self._zxt_120 == 1:
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data
self._swing_list = list(value.data_items)
_LOGGER.debug("self._swing_list=%s", self._swing_list)
_LOGGER.debug("self._current_swing_mode=%s",
self._current_swing_mode)
@property
def should_poll(self):
"""No polling on ZWave."""
return False
@property
def current_fan_mode(self):
"""Return the fan speed set."""
return self._current_fan_mode
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
@property
def current_swing_mode(self):
"""Return the swing mode set."""
return self._current_swing_mode
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
unit = self._unit
if unit == 'C':
return TEMP_CELSIUS
elif unit == 'F':
return TEMP_FAHRENHEIT
else:
_LOGGER.exception("unit_of_measurement=%s is not valid",
unit)
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def current_operation(self):
"""Return the current operation mode."""
return self._current_operation
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if self.current_operation is not None:
if SET_TEMP_TO_INDEX.get(self._current_operation) \
!= value.index:
continue
_LOGGER.debug("SET_TEMP_TO_INDEX=%s and"
" self._current_operation=%s",
SET_TEMP_TO_INDEX.get(self._current_operation),
self._current_operation)
if self._zxt_120:
# ZXT-120 does not support get setpoint
self._target_temperature = temperature
# ZXT-120 responds only to whole int
value.data = int(round(temperature, 0))
else:
value.data = int(temperature)
break
else:
value.data = int(temperature)
break
def set_fan_mode(self, fan):
"""Set new target fan mode."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
if value.command_class == 68 and value.index == 0:
value.data = bytes(fan, 'utf-8')
break
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
if value.command_class == 64 and value.index == 0:
value.data = bytes(operation_mode, 'utf-8')
break
def set_swing_mode(self, swing_mode):
"""Set new target swing mode."""
if self._zxt_120 == 1:
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
value.data = bytes(swing_mode, 'utf-8')
break
+18 -18
View File
@@ -11,26 +11,26 @@ import logging
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import generate_entity_id
DOMAIN = "configurator"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SERVICE_CONFIGURE = "configure"
STATE_CONFIGURE = "configure"
STATE_CONFIGURED = "configured"
ATTR_LINK_NAME = "link_name"
ATTR_LINK_URL = "link_url"
ATTR_CONFIGURE_ID = "configure_id"
ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_IMAGE = "description_image"
ATTR_SUBMIT_CAPTION = "submit_caption"
ATTR_FIELDS = "fields"
ATTR_ERRORS = "errors"
_REQUESTS = {}
_INSTANCES = {}
_LOGGER = logging.getLogger(__name__)
_REQUESTS = {}
ATTR_CONFIGURE_ID = 'configure_id'
ATTR_DESCRIPTION = 'description'
ATTR_DESCRIPTION_IMAGE = 'description_image'
ATTR_ERRORS = 'errors'
ATTR_FIELDS = 'fields'
ATTR_LINK_NAME = 'link_name'
ATTR_LINK_URL = 'link_url'
ATTR_SUBMIT_CAPTION = 'submit_caption'
DOMAIN = 'configurator'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_CONFIGURE = 'configure'
STATE_CONFIGURE = 'configure'
STATE_CONFIGURED = 'configured'
# pylint: disable=too-many-arguments
+7 -7
View File
@@ -15,20 +15,20 @@ from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
import homeassistant.helpers.config_validation as cv
DOMAIN = "conversation"
REQUIREMENTS = ['fuzzywuzzy==0.11.1']
SERVICE_PROCESS = "process"
ATTR_TEXT = 'text'
ATTR_TEXT = "text"
DOMAIN = 'conversation'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
SERVICE_PROCESS = 'process'
SERVICE_PROCESS_SCHEMA = vol.Schema({
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
})
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REQUIREMENTS = ['fuzzywuzzy==0.11.0']
def setup(hass, config):
"""Register the process service."""
+236
View File
@@ -0,0 +1,236 @@
"""
Support for Cover devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover/
"""
import os
import logging
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.components import group
from homeassistant.const import (
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION, STATE_OPEN,
STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID)
DOMAIN = 'cover'
SCAN_INTERVAL = 15
GROUP_NAME_ALL_COVERS = 'all_covers'
ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_COVERS)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
_LOGGER = logging.getLogger(__name__)
ATTR_CURRENT_POSITION = 'current_position'
ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
ATTR_POSITION = 'position'
ATTR_TILT_POSITION = 'tilt_position'
COVER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
COVER_SET_COVER_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_POSITION):
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
})
COVER_SET_COVER_TILT_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_TILT_POSITION):
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
})
SERVICE_TO_METHOD = {
SERVICE_OPEN_COVER: {'method': 'open_cover'},
SERVICE_CLOSE_COVER: {'method': 'close_cover'},
SERVICE_SET_COVER_POSITION: {
'method': 'set_cover_position',
'schema': COVER_SET_COVER_POSITION_SCHEMA},
SERVICE_STOP_COVER: {'method': 'stop_cover'},
SERVICE_OPEN_COVER_TILT: {'method': 'open_cover_tilt'},
SERVICE_CLOSE_COVER_TILT: {'method': 'close_cover_tilt'},
SERVICE_SET_COVER_TILT_POSITION: {
'method': 'set_cover_tilt_position',
'schema': COVER_SET_COVER_TILT_POSITION_SCHEMA},
}
def is_closed(hass, entity_id=None):
"""Return if the cover is closed based on the statemachine."""
entity_id = entity_id or ENTITY_ID_ALL_COVERS
return hass.states.is_state(entity_id, STATE_CLOSED)
def open_cover(hass, entity_id=None):
"""Open all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_OPEN_COVER, data)
def close_cover(hass, entity_id=None):
"""Close all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, data)
def set_cover_position(hass, position, entity_id=None):
"""Move to specific position all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
data[ATTR_POSITION] = position
hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, data)
def stop_cover(hass, entity_id=None):
"""Stop all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_STOP_COVER, data)
def open_cover_tilt(hass, entity_id=None):
"""Open all or specified cover tilt."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_OPEN_COVER_TILT, data)
def close_cover_tilt(hass, entity_id=None):
"""Close all or specified cover tilt."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_CLOSE_COVER_TILT, data)
def set_cover_tilt_position(hass, tilt_position, entity_id=None):
"""Move to specific tilt position all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
data[ATTR_TILT_POSITION] = tilt_position
hass.services.call(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data)
def stop_cover_tilt(hass, entity_id=None):
"""Stop all or specified cover tilt."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data)
def setup(hass, config):
"""Track states and offer events for covers."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
component.setup(config)
def handle_cover_service(service):
"""Handle calls to the cover services."""
method = SERVICE_TO_METHOD.get(service.service)
params = service.data.copy()
params.pop(ATTR_ENTITY_ID, None)
if method:
for cover in component.extract_from_service(service):
getattr(cover, method['method'])(**params)
if cover.should_poll:
cover.update_ha_state(True)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service_name].get(
'schema', COVER_SERVICE_SCHEMA)
hass.services.register(DOMAIN, service_name, handle_cover_service,
descriptions.get(service_name), schema=schema)
return True
class CoverDevice(Entity):
"""Representation a cover."""
# pylint: disable=no-self-use
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
pass
@property
def current_cover_tilt_position(self):
"""Return current position of cover tilt.
None is unknown, 0 is closed, 100 is fully open.
"""
pass
@property
def state(self):
"""Return the state of the cover."""
closed = self.is_closed
if closed is None:
return STATE_UNKNOWN
return STATE_CLOSED if closed else STATE_OPEN
@property
def state_attributes(self):
"""Return the state attributes."""
data = {}
current = self.current_cover_position
if current is not None:
data[ATTR_CURRENT_POSITION] = self.current_cover_position
current_tilt = self.current_cover_tilt_position
if current_tilt is not None:
data[ATTR_CURRENT_TILT_POSITION] = self.current_cover_tilt_position
return data
@property
def is_closed(self):
"""Return if the cover is closed or not."""
raise NotImplementedError()
def open_cover(self, **kwargs):
"""Open the cover."""
raise NotImplementedError()
def close_cover(self, **kwargs):
"""Close cover."""
raise NotImplementedError()
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
pass
def stop_cover(self, **kwargs):
"""Stop the cover."""
pass
def open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
pass
def close_cover_tilt(self, **kwargs):
"""Close the cover tilt."""
pass
def set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
pass
def stop_cover_tilt(self, **kwargs):
"""Stop the cover."""
pass
@@ -0,0 +1,128 @@
"""
Support for command line covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.command_line/
"""
import logging
import subprocess
from homeassistant.components.cover import CoverDevice
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers import template
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup cover controlled by shell commands."""
covers = config.get('covers', {})
devices = []
for dev_name, properties in covers.items():
devices.append(
CommandCover(
hass,
properties.get('name', dev_name),
properties.get('opencmd', 'true'),
properties.get('closecmd', 'true'),
properties.get('stopcmd', 'true'),
properties.get('statecmd', False),
properties.get(CONF_VALUE_TEMPLATE, '{{ value }}')))
add_devices_callback(devices)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class CommandCover(CoverDevice):
"""Representation a command line cover."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, command_open, command_close, command_stop,
command_state, value_template):
"""Initialize the cover."""
self._hass = hass
self._name = name
self._state = None
self._command_open = command_open
self._command_close = command_close
self._command_stop = command_stop
self._command_state = command_state
self._value_template = value_template
@staticmethod
def _move_cover(command):
"""Execute the actual commands."""
_LOGGER.info('Running command: %s', command)
success = (subprocess.call(command, shell=True) == 0)
if not success:
_LOGGER.error('Command failed: %s', command)
return success
@staticmethod
def _query_state_value(command):
"""Execute state command for return value."""
_LOGGER.info('Running state command: %s', command)
try:
return_value = subprocess.check_output(command, shell=True)
return return_value.strip().decode('utf-8')
except subprocess.CalledProcessError:
_LOGGER.error('Command failed: %s', command)
@property
def should_poll(self):
"""Only poll if we have state command."""
return self._command_state is not None
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._state
def _query_state(self):
"""Query for the state."""
if not self._command_state:
_LOGGER.error('No state command specified')
return
return self._query_state_value(self._command_state)
def update(self):
"""Update device state."""
if self._command_state:
payload = str(self._query_state())
if self._value_template:
payload = template.render_with_possible_json_value(
self._hass, self._value_template, payload)
self._state = int(payload)
def open_cover(self, **kwargs):
"""Open the cover."""
self._move_cover(self._command_open)
def close_cover(self, **kwargs):
"""Close the cover."""
self._move_cover(self._command_close)
def stop_cover(self, **kwargs):
"""Stop the cover."""
self._move_cover(self._command_stop)
+173
View File
@@ -0,0 +1,173 @@
"""
Demo platform for the cover component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.cover import CoverDevice
from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.helpers.event import track_utc_time_change
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo covers."""
add_devices([
DemoCover(hass, 'Kitchen Window'),
DemoCover(hass, 'Hall Window', 10),
DemoCover(hass, 'Living Room Window', 70, 50),
])
class DemoCover(CoverDevice):
"""Representation of a demo cover."""
# pylint: disable=no-self-use, too-many-instance-attributes
def __init__(self, hass, name, position=None, tilt_position=None):
"""Initialize the cover."""
self.hass = hass
self._name = name
self._position = position
self._set_position = None
self._set_tilt_position = None
self._tilt_position = tilt_position
self._closing = True
self._closing_tilt = True
self._listener_cover = None
self._listener_cover_tilt = None
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def should_poll(self):
"""No polling needed for a demo cover."""
return False
@property
def current_cover_position(self):
"""Return the current position of the cover."""
return self._position
@property
def current_cover_tilt_position(self):
"""Return the current tilt position of the cover."""
return self._tilt_position
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._position is not None:
if self.current_cover_position > 0:
return False
else:
return True
else:
return None
def close_cover(self, **kwargs):
"""Close the cover."""
if self._position in (0, None):
return
self._listen_cover()
self._closing = True
def close_cover_tilt(self, **kwargs):
"""Close the cover tilt."""
if self._tilt_position in (0, None):
return
self._listen_cover_tilt()
self._closing_tilt = True
def open_cover(self, **kwargs):
"""Open the cover."""
if self._position in (100, None):
return
self._listen_cover()
self._closing = False
def open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
if self._tilt_position in (100, None):
return
self._listen_cover_tilt()
self._closing_tilt = False
def set_cover_position(self, position, **kwargs):
"""Move the cover to a specific position."""
self._set_position = round(position, -1)
if self._position == position:
return
self._listen_cover()
self._closing = position < self._position
def set_cover_tilt_position(self, tilt_position, **kwargs):
"""Move the cover til to a specific position."""
self._set_tilt_position = round(tilt_position, -1)
if self._tilt_position == tilt_position:
return
self._listen_cover_tilt()
self._closing_tilt = tilt_position < self._tilt_position
def stop_cover(self, **kwargs):
"""Stop the cover."""
if self._position is None:
return
if self._listener_cover is not None:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED,
self._listener_cover)
self._listener_cover = None
self._set_position = None
def stop_cover_tilt(self, **kwargs):
"""Stop the cover tilt."""
if self._tilt_position is None:
return
if self._listener_cover_tilt is not None:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED,
self._listener_cover_tilt)
self._listener_cover_tilt = None
self._set_tilt_position = None
def _listen_cover(self):
"""Listen for changes in cover."""
if self._listener_cover is None:
self._listener_cover = track_utc_time_change(
self.hass, self._time_changed_cover)
def _time_changed_cover(self, now):
"""Track time changes."""
if self._closing:
self._position -= 10
else:
self._position += 10
if self._position in (100, 0, self._set_position):
self.stop_cover()
self.update_ha_state()
def _listen_cover_tilt(self):
"""Listen for changes in cover tilt."""
if self._listener_cover_tilt is None:
self._listener_cover_tilt = track_utc_time_change(
self.hass, self._time_changed_cover_tilt)
def _time_changed_cover_tilt(self, now):
"""Track time changes."""
if self._closing_tilt:
self._tilt_position -= 10
else:
self._tilt_position += 10
if self._tilt_position in (100, 0, self._set_tilt_position):
self.stop_cover_tilt()
self.update_ha_state()
+101
View File
@@ -0,0 +1,101 @@
"""
The homematic cover platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.homematic/
Important: For this platform to work the homematic component has to be
properly configured.
"""
import logging
from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.cover import CoverDevice,\
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(HMCover,
discovery_info,
add_callback_devices)
# pylint: disable=abstract-method
class HMCover(homematic.HMDevice, CoverDevice):
"""Represents a Homematic Cover in Home Assistant."""
@property
def current_cover_position(self):
"""
Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
if self.available:
return int((1 - self._hm_get_state()) * 100)
return None
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
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 is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
def open_cover(self, **kwargs):
"""Open the cover."""
if self.available:
self._hmdevice.move_up(self._channel)
def close_cover(self, **kwargs):
"""Close the cover."""
if self.available:
self._hmdevice.move_down(self._channel)
def stop_cover(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 cover!", 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})
+167
View File
@@ -0,0 +1,167 @@
"""
Support for MQTT cover devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.mqtt/
"""
import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt
from homeassistant.components.cover import CoverDevice
from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
STATE_CLOSED)
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt']
CONF_PAYLOAD_OPEN = 'payload_open'
CONF_PAYLOAD_CLOSE = 'payload_close'
CONF_PAYLOAD_STOP = 'payload_stop'
CONF_STATE_OPEN = 'state_open'
CONF_STATE_CLOSED = 'state_closed'
DEFAULT_NAME = "MQTT Cover"
DEFAULT_PAYLOAD_OPEN = "OPEN"
DEFAULT_PAYLOAD_CLOSE = "CLOSE"
DEFAULT_PAYLOAD_STOP = "STOP"
DEFAULT_OPTIMISTIC = False
DEFAULT_RETAIN = False
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Add MQTT Cover."""
add_devices_callback([MqttCover(
hass,
config[CONF_NAME],
config.get(CONF_STATE_TOPIC),
config[CONF_COMMAND_TOPIC],
config[CONF_QOS],
config[CONF_RETAIN],
config[CONF_STATE_OPEN],
config[CONF_STATE_CLOSED],
config[CONF_PAYLOAD_OPEN],
config[CONF_PAYLOAD_CLOSE],
config[CONF_PAYLOAD_STOP],
config[CONF_OPTIMISTIC],
config.get(CONF_VALUE_TEMPLATE)
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttCover(CoverDevice):
"""Representation of a cover that can be controlled using MQTT."""
def __init__(self, hass, name, state_topic, command_topic, qos,
retain, state_open, state_closed, payload_open, payload_close,
payload_stop, optimistic, value_template):
"""Initialize the cover."""
self._position = None
self._state = None
self._hass = hass
self._name = name
self._state_topic = state_topic
self._command_topic = command_topic
self._qos = qos
self._payload_open = payload_open
self._payload_close = payload_close
self._payload_stop = payload_stop
self._state_open = state_open
self._state_closed = state_closed
self._retain = retain
self._optimistic = optimistic or state_topic is None
def message_received(topic, payload, qos):
"""A new MQTT message has been received."""
if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
if payload == self._state_open:
self._state = False
self.update_ha_state()
elif payload == self._state_closed:
self._state = True
self.update_ha_state()
elif payload.isnumeric() and 0 <= int(payload) <= 100:
self._state = int(payload)
self._position = int(payload)
self.update_ha_state()
else:
_LOGGER.warning(
"Payload is not True or False or"
" integer(0-100) %s", payload)
if self._state_topic is None:
# Force into optimistic mode.
self._optimistic = True
else:
mqtt.subscribe(hass, self._state_topic, message_received,
self._qos)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._position
def open_cover(self, **kwargs):
"""Move the cover up."""
mqtt.publish(self.hass, self._command_topic, self._payload_open,
self._qos, self._retain)
if self._optimistic:
# Optimistically assume that cover has changed state.
self._state = 100
self.update_ha_state()
def close_cover(self, **kwargs):
"""Move the cover down."""
mqtt.publish(self.hass, self._command_topic, self._payload_close,
self._qos, self._retain)
if self._optimistic:
# Optimistically assume that cover has changed state.
self._state = 0
self.update_ha_state()
def stop_cover(self, **kwargs):
"""Stop the device."""
mqtt.publish(self.hass, self._command_topic, self._payload_stop,
self._qos, self._retain)
+67
View File
@@ -0,0 +1,67 @@
"""
Support for RFXtrx cover components.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.rfxtrx/
"""
import homeassistant.components.rfxtrx as rfxtrx
from homeassistant.components.cover import CoverDevice
DEPENDENCIES = ['rfxtrx']
PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the RFXtrx cover."""
import RFXtrx as rfxtrxmod
# Add cover from config file
covers = rfxtrx.get_devices_from_config(config,
RfxtrxCover)
add_devices_callback(covers)
def cover_update(event):
"""Callback for cover updates from the RFXtrx gateway."""
if not isinstance(event.device, rfxtrxmod.LightingDevice) or \
event.device.known_to_be_dimmable or \
not event.device.known_to_be_rollershutter:
return
new_device = rfxtrx.get_new_device(event, config, RfxtrxCover)
if new_device:
add_devices_callback([new_device])
rfxtrx.apply_received_command(event)
# Subscribe to main rfxtrx events
if cover_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update)
# pylint: disable=abstract-method
class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice):
"""Representation of an rfxtrx cover."""
@property
def should_poll(self):
"""No polling available in rfxtrx cover."""
return False
@property
def is_closed(self):
"""Return if the cover is closed."""
return None
def open_cover(self, **kwargs):
"""Move the cover up."""
self._send_command("roll_up")
def close_cover(self, **kwargs):
"""Move the cover down."""
self._send_command("roll_down")
def stop_cover(self, **kwargs):
"""Stop the cover."""
self._send_command("stop_roll")
+112
View File
@@ -0,0 +1,112 @@
"""
Support for building a Raspberry Pi cover 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/cover.rpi_gpio/
"""
import logging
from time import sleep
import voluptuous as vol
from homeassistant.components.cover import CoverDevice
import homeassistant.components.rpi_gpio as rpi_gpio
import homeassistant.helpers.config_validation as cv
RELAY_TIME = 'relay_time'
STATE_PULL_MODE = 'state_pull_mode'
DEFAULT_PULL_MODE = 'UP'
DEFAULT_RELAY_TIME = .2
DEPENDENCIES = ['rpi_gpio']
_LOGGER = logging.getLogger(__name__)
_COVERS_SCHEMA = vol.All(
cv.ensure_list,
[
vol.Schema({
'name': str,
'relay_pin': int,
'state_pin': int,
})
]
)
PLATFORM_SCHEMA = vol.Schema({
'platform': str,
vol.Required('covers'): _COVERS_SCHEMA,
vol.Optional(STATE_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string,
vol.Optional(RELAY_TIME, default=DEFAULT_RELAY_TIME): vol.Coerce(int),
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the cover platform."""
relay_time = config.get(RELAY_TIME)
state_pull_mode = config.get(STATE_PULL_MODE)
covers = []
covers_conf = config.get('covers')
for cover in covers_conf:
covers.append(RPiGPIOCover(cover['name'], cover['relay_pin'],
cover['state_pin'],
state_pull_mode,
relay_time))
add_devices(covers)
# pylint: disable=abstract-method
class RPiGPIOCover(CoverDevice):
"""Representation of a Raspberry cover."""
# pylint: disable=too-many-arguments
def __init__(self, name, relay_pin, state_pin,
state_pull_mode, relay_time):
"""Initialize the cover."""
self._name = name
self._state = False
self._relay_pin = relay_pin
self._state_pin = state_pin
self._state_pull_mode = state_pull_mode
self._relay_time = relay_time
rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
rpi_gpio.write_output(self._relay_pin, True)
@property
def unique_id(self):
"""Return the ID of this cover."""
return "{}.{}".format(self.__class__, self._name)
@property
def name(self):
"""Return the name of the cover if any."""
return self._name
def update(self):
"""Update the state of the cover."""
self._state = rpi_gpio.read_input(self._state_pin)
@property
def is_closed(self):
"""Return true if cover is closed."""
return self._state
def _trigger(self):
"""Trigger the cover."""
rpi_gpio.write_output(self._relay_pin, False)
sleep(self._relay_time)
rpi_gpio.write_output(self._relay_pin, True)
def close_cover(self):
"""Close the cover."""
if not self.is_closed:
self._trigger()
def open_cover(self):
"""Open the cover."""
if self.is_closed:
self._trigger()
+96
View File
@@ -0,0 +1,96 @@
"""
Allow to configure a SCSGate cover.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.scsgate/
"""
import logging
import homeassistant.components.scsgate as scsgate
from homeassistant.components.cover import CoverDevice
from homeassistant.const import CONF_NAME
DEPENDENCIES = ['scsgate']
SCS_ID = 'scs_id'
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the SCSGate cover."""
devices = config.get('devices')
covers = []
logger = logging.getLogger(__name__)
if devices:
for _, entity_info in devices.items():
if entity_info[SCS_ID] in scsgate.SCSGATE.devices:
continue
logger.info("Adding %s scsgate.cover", entity_info[CONF_NAME])
name = entity_info[CONF_NAME]
scs_id = entity_info[SCS_ID]
cover = SCSGateCover(
name=name,
scs_id=scs_id,
logger=logger)
scsgate.SCSGATE.add_device(cover)
covers.append(cover)
add_devices_callback(covers)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class SCSGateCover(CoverDevice):
"""Representation of SCSGate cover."""
def __init__(self, scs_id, name, logger):
"""Initialize the cover."""
self._scs_id = scs_id
self._name = name
self._logger = logger
@property
def scs_id(self):
"""Return the SCSGate ID."""
return self._scs_id
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
return None
def open_cover(self, **kwargs):
"""Move the cover."""
from scsgate.tasks import RaiseRollerShutterTask
scsgate.SCSGATE.append_task(
RaiseRollerShutterTask(target=self._scs_id))
def close_cover(self, **kwargs):
"""Move the cover down."""
from scsgate.tasks import LowerRollerShutterTask
scsgate.SCSGATE.append_task(
LowerRollerShutterTask(target=self._scs_id))
def stop_cover(self, **kwargs):
"""Stop the cover."""
from scsgate.tasks import HaltRollerShutterTask
scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id))
def process_event(self, message):
"""Handle a SCSGate message related with this cover."""
self._logger.debug(
"Rollershutter %s, got message %s",
self._scs_id, message.toggled)
@@ -0,0 +1,71 @@
open_cover:
description: Open all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to open
example: 'cover.living_room'
close_cover:
description: Close all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to close
example: 'cover.living_room'
set_cover_position:
description: Move to specific position all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to set cover position
example: 'cover.living_room'
position:
description: Position of the cover (0 to 100)
example: 30
stop_cover:
description: Stop all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to stop
example: 'cover.living_room'
open_cover_tilt:
description: Open all or specified cover tilt
fields:
entity_id:
description: Name(s) of cover(s) tilt to open
example: 'cover.living_room'
close_cover_tilt:
description: Close all or specified cover tilt
fields:
entity_id:
description: Name(s) of cover(s) to close tilt
example: 'cover.living_room'
set_cover_tilt_position:
description: Move to specific position all or specified cover tilt
fields:
entity_id:
description: Name(s) of cover(s) to set cover tilt position
example: 'cover.living_room'
position:
description: Position of the cover (0 to 100)
example: 30
stop_cover_tilt:
description: Stop all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to stop
example: 'cover.living_room'
+64
View File
@@ -0,0 +1,64 @@
"""
Support for Wink Covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.wink/
"""
import logging
from homeassistant.components.cover import CoverDevice
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.13', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink cover platform."""
import pywink
if discovery_info is None:
token = config.get(CONF_ACCESS_TOKEN)
if token is None:
logging.getLogger(__name__).error(
"Missing wink access_token. "
"Get one at https://winkbearertoken.appspot.com/")
return
pywink.set_bearer_token(token)
add_devices(WinkCoverDevice(shade) for shade, door in
pywink.get_shades())
class WinkCoverDevice(WinkDevice, CoverDevice):
"""Representation of a Wink covers."""
def __init__(self, wink):
"""Initialize the cover."""
WinkDevice.__init__(self, wink)
@property
def should_poll(self):
"""Wink Shades don't track their position."""
return False
def close_cover(self):
"""Close the shade."""
self.wink.set_state(0)
def open_cover(self):
"""Open the shade."""
self.wink.set_state(1)
@property
def is_closed(self):
"""Return if the cover is closed."""
state = self.wink.state()
if state == 0:
return True
elif state == 1:
return False
else:
return None
+184
View File
@@ -0,0 +1,184 @@
"""
Support for Zwave cover components.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.cover import DOMAIN
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.cover import CoverDevice
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
SOMFY = 0x47
SOMFY_ZRTSI = 0x5a52
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
WORKAROUND = 'workaround'
DEVICE_MAPPINGS = {
SOMFY_ZRTSI_CONTROLLER: WORKAROUND
}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Z-Wave covers."""
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 and
value.index == 0):
value.set_change_verified(False)
add_devices([ZwaveRollershutter(value)])
elif (value.command_class == zwave.COMMAND_CLASS_SWITCH_BINARY or
value.command_class == zwave.COMMAND_CLASS_BARRIER_OPERATOR):
if value.type != zwave.TYPE_BOOL and \
value.genre != zwave.GENRE_USER:
return
value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)])
else:
return
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Representation of an Zwave roller shutter."""
def __init__(self, value):
"""Initialize the zwave rollershutter."""
import libopenzwave
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._lozwmgr = libopenzwave.PyManager()
self._lozwmgr.create()
self._node = value.node
self._current_position = None
self._workaround = None
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
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_type, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND:
_LOGGER.debug("Controller without positioning feedback")
self._workaround = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Position value
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Level':
self._current_position = value.data
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position > 0:
return False
else:
return True
@property
def current_cover_position(self):
"""Return the current position of Zwave roller shutter."""
if not self._workaround:
if self._current_position is not None:
if self._current_position <= 5:
return 0
elif self._current_position >= 95:
return 100
else:
return self._current_position
def open_cover(self, **kwargs):
"""Move the roller shutter up."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Open' or \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Down':
self._lozwmgr.pressButton(value.value_id)
break
def close_cover(self, **kwargs):
"""Move the roller shutter down."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Up' or \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Close':
self._lozwmgr.pressButton(value.value_id)
break
def set_cover_position(self, position, **kwargs):
"""Move the roller shutter to a specific position."""
self._node.set_dimmer(self._value.value_id, 100 - position)
def stop_cover(self, **kwargs):
"""Stop the roller shutter."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Open' or \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Down':
self._lozwmgr.releaseButton(value.value_id)
break
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
"""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._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()
_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_cover(self):
"""Close the garage door."""
self._value.data = False
def open_cover(self):
"""Open the garage door."""
self._value.data = True
+2 -4
View File
@@ -11,17 +11,16 @@ import homeassistant.core as ha
import homeassistant.loader as loader
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
DOMAIN = "demo"
DEPENDENCIES = ['conversation', 'introduction', 'zone']
DOMAIN = 'demo'
COMPONENTS_WITH_DEMO_PLATFORM = [
'alarm_control_panel',
'binary_sensor',
'camera',
'climate',
'device_tracker',
'garage_door',
'hvac',
'light',
'lock',
'media_player',
@@ -29,7 +28,6 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
'rollershutter',
'sensor',
'switch',
'thermostat',
]
@@ -10,14 +10,19 @@ from datetime import timedelta
import logging
import os
import threading
from typing import Any, Sequence, Callable
from homeassistant.bootstrap import prepare_setup_platform
import voluptuous as vol
from homeassistant.bootstrap import (
prepare_setup_platform, log_exception)
from homeassistant.components import group, zone
from homeassistant.components.discovery import SERVICE_NETGEAR
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
import homeassistant.util.dt as dt_util
@@ -27,8 +32,7 @@ from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
DOMAIN = "device_tracker"
DOMAIN = 'device_tracker'
DEPENDENCIES = ['zone']
GROUP_NAME_ALL_DEVICES = 'all devices'
@@ -38,21 +42,18 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
YAML_DEVICES = 'known_devices.yaml'
CONF_TRACK_NEW = "track_new_devices"
DEFAULT_CONF_TRACK_NEW = True
CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = 180 # seconds
CONF_SCAN_INTERVAL = "interval_seconds"
CONF_SCAN_INTERVAL = 'interval_seconds'
DEFAULT_SCAN_INTERVAL = 12
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
CONF_HOME_RANGE = 'home_range'
DEFAULT_HOME_RANGE = 100
SERVICE_SEE = 'see'
ATTR_MAC = 'mac'
@@ -62,23 +63,33 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_GPS = 'gps'
ATTR_BATTERY = 'battery'
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds
}, extra=vol.ALLOW_EXTRA)
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
vol.Schema({
vol.Optional(CONF_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME): cv.positive_int # seconds
}, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA)
DISCOVERY_PLATFORMS = {
SERVICE_NETGEAR: 'netgear',
}
_LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-arguments
def is_on(hass, entity_id=None):
def is_on(hass: HomeAssistantType, entity_id: str=None):
"""Return the state if any or a specified device is home."""
entity = entity_id or ENTITY_ID_ALL_DEVICES
return hass.states.is_state(entity, STATE_HOME)
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
gps=None, gps_accuracy=None, battery=None):
def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=None,
battery=None): # pylint: disable=too-many-arguments
"""Call service to notify you see device."""
data = {key: value for key, value in
((ATTR_MAC, mac),
@@ -91,27 +102,24 @@ def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
hass.services.call(DOMAIN, SERVICE_SEE, data)
def setup(hass, config):
def setup(hass: HomeAssistantType, config: ConfigType):
"""Setup device tracker."""
yaml_path = hass.config.path(YAML_DEVICES)
conf = config.get(DOMAIN, {})
# Config can be an empty list. In that case, substitute a dict
if isinstance(conf, list):
try:
conf = _CONFIG_SCHEMA(config).get(DOMAIN, [])
except vol.Invalid as ex:
log_exception(ex, DOMAIN, config)
return False
else:
conf = conf[0] if len(conf) > 0 else {}
consider_home = timedelta(
seconds=conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME))
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
consider_home = timedelta(
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
DEFAULT_CONSIDER_HOME))
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
DEFAULT_CONF_TRACK_NEW)
home_range = util.convert(conf.get(CONF_HOME_RANGE), int,
DEFAULT_HOME_RANGE)
devices = load_config(yaml_path, hass, consider_home)
devices = load_config(yaml_path, hass, consider_home, home_range)
tracker = DeviceTracker(hass, consider_home, track_new, home_range,
devices)
tracker = DeviceTracker(hass, consider_home, track_new, devices)
def setup_platform(p_type, p_config, disc_info=None):
"""Setup a device tracker platform."""
@@ -170,30 +178,37 @@ def setup(hass, config):
class DeviceTracker(object):
"""Representation of a device tracker."""
def __init__(self, hass, consider_home, track_new, home_range, devices):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track_new: bool, devices: Sequence) -> None:
"""Initialize a device tracker."""
self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
for dev in devices:
if self.devices[dev.dev_id] is not dev:
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
_LOGGER.warning('Duplicate device MAC addresses detected %s',
dev.mac)
self.consider_home = consider_home
self.track_new = track_new
self.home_range = home_range
self.lock = threading.Lock()
for device in devices:
if device.track:
device.update_ha_state()
self.group = None
self.group = None # type: group.Group
def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
gps=None, gps_accuracy=None, battery=None):
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
battery: str=None):
"""Notify the device tracker that you see a device."""
with self.lock:
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
elif mac is not None:
mac = mac.upper()
mac = str(mac).upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
@@ -211,7 +226,7 @@ class DeviceTracker(object):
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device(
self.hass, self.consider_home, self.home_range, self.track_new,
self.hass, self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '))
self.devices[dev_id] = device
if mac is not None:
@@ -234,7 +249,7 @@ class DeviceTracker(object):
self.group = group.Group(
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
def update_stale(self, now):
def update_stale(self, now: dt_util.dt.datetime):
"""Update stale devices."""
with self.lock:
for device in self.devices.values():
@@ -246,19 +261,21 @@ class DeviceTracker(object):
class Device(Entity):
"""Represent a tracked device."""
host_name = None
location_name = None
gps = None
host_name = None # type: str
location_name = None # type: str
gps = None # type: GPSType
gps_accuracy = 0
last_seen = None
battery = None
last_seen = None # type: dt_util.dt.datetime
battery = None # type: str
# Track if the last update of this device was HOME.
last_update_home = False
_state = STATE_NOT_HOME
def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
name=None, picture=None, away_hide=False):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str=None,
picture: str=None, gravatar: str=None,
away_hide: bool=False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@@ -267,8 +284,6 @@ class Device(Entity):
# detected anymore.
self.consider_home = consider_home
# Distance in meters
self.home_range = home_range
# Device ID
self.dev_id = dev_id
self.mac = mac
@@ -280,15 +295,12 @@ class Device(Entity):
self.config_name = name
# Configured picture
self.config_picture = picture
self.away_hide = away_hide
if gravatar is not None:
self.config_picture = get_gravatar_for_email(gravatar)
else:
self.config_picture = picture
@property
def gps_home(self):
"""Return if device is within range of home."""
distance = max(
0, self.hass.config.distance(*self.gps) - self.gps_accuracy)
return self.gps is not None and distance <= self.home_range
self.away_hide = away_hide
@property
def name(self):
@@ -325,26 +337,24 @@ class Device(Entity):
"""If device should be hidden."""
return self.away_hide and self.state != STATE_HOME
def seen(self, host_name=None, location_name=None, gps=None,
gps_accuracy=0, battery=None):
def seen(self, host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=0, battery: str=None):
"""Mark the device as seen."""
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.gps_accuracy = gps_accuracy or 0
self.battery = battery
if gps is None:
self.gps = None
else:
self.gps = None
if gps is not None:
try:
self.gps = tuple(float(val) for val in gps)
except ValueError:
self.gps = float(gps[0]), float(gps[1])
except (ValueError, TypeError, IndexError):
_LOGGER.warning('Could not parse gps value for %s: %s',
self.dev_id, gps)
self.gps = None
self.update()
def stale(self, now=None):
def stale(self, now: dt_util.dt.datetime=None):
"""Return if device state is stale."""
return self.last_seen and \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
@@ -373,31 +383,30 @@ class Device(Entity):
self.last_update_home = True
def load_config(path, hass, consider_home, home_range):
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file."""
if not os.path.isfile(path):
return []
try:
return [
Device(hass, consider_home, home_range, device.get('track', False),
Device(hass, consider_home, device.get('track', False),
str(dev_id).lower(), str(device.get('mac')).upper(),
device.get('name'), device.get('picture'),
device.get('gravatar'),
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
for dev_id, device in load_yaml_config_file(path).items()]
except HomeAssistantError:
except (HomeAssistantError, FileNotFoundError):
# When YAML file could not be loaded/did not contain a dict
return []
def setup_scanner_platform(hass, config, scanner, see_device):
def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, see_device: Callable):
"""Helper method to connect scanner-based platform to device tracker."""
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
DEFAULT_SCAN_INTERVAL)
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
# Initial scan of each mac we also tell about host name for config
seen = set()
seen = set() # type: Any
def device_tracker_scan(now):
def device_tracker_scan(now: dt_util.dt.datetime):
"""Called when interval matches."""
for mac in scanner.scan_devices():
if mac in seen:
@@ -413,7 +422,7 @@ def setup_scanner_platform(hass, config, scanner, see_device):
device_tracker_scan(None)
def update_config(path, dev_id, device):
def update_config(path: str, dev_id: str, device: Device):
"""Add device to YAML configuration file."""
with open(path, 'a') as out:
out.write('\n')
@@ -425,3 +434,10 @@ def update_config(path, dev_id, device):
(CONF_AWAY_HIDE,
'yes' if device.away_hide else 'no')):
out.write(' {}: {}\n'.format(key, '' if value is None else value))
def get_gravatar_for_email(email: str):
"""Return an 80px Gravatar for the given email address."""
import hashlib
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())
@@ -1,126 +1,129 @@
"""
Support for Actiontec MI424WR (Verizon FIOS) routers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.actiontec/
"""
import logging
import re
import telnetlib
import threading
from collections import namedtuple
from datetime import timedelta
import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
r'\ssec')
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an Actiontec scanner."""
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "ip", "last_update"])
class ActiontecDeviceScanner(object):
"""This class queries a an actiontec router for connected devices."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = []
data = self.get_actiontec_data()
self.success_init = data is not None
_LOGGER.info("actiontec scanner initialized")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [client.mac for client in self.last_results]
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
if not self.last_results:
return None
for client in self.last_results:
if client.mac == device:
return client.ip
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the router is up to date.
Return boolean if scanning successful.
"""
_LOGGER.info("Scanning")
if not self.success_init:
return False
with self.lock:
now = dt_util.now()
actiontec_data = self.get_actiontec_data()
if not actiontec_data:
return False
self.last_results = [Device(data['mac'], name, now)
for name, data in actiontec_data.items()
if data['timevalid'] > -60]
_LOGGER.info("actiontec scan successful")
return True
def get_actiontec_data(self):
"""Retrieve data from Actiontec MI424WR and return parsed result."""
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'Username: ')
telnet.write((self.username + '\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt = telnet.read_until(
b'Wireless Broadband Router> ').split(b'\n')[-1]
telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
telnet.write('\n'.encode('ascii'))
telnet.read_until(prompt)
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return None
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if match is not None:
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'timevalid': int(match.group('timevalid'))
}
return devices
"""
Support for Actiontec MI424WR (Verizon FIOS) routers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.actiontec/
"""
import logging
import re
import telnetlib
import threading
from collections import namedtuple
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
r'\ssec')
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an Actiontec scanner."""
scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "ip", "last_update"])
class ActiontecDeviceScanner(object):
"""This class queries a an actiontec router for connected devices."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = []
data = self.get_actiontec_data()
self.success_init = data is not None
_LOGGER.info("actiontec scanner initialized")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [client.mac for client in self.last_results]
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
if not self.last_results:
return None
for client in self.last_results:
if client.mac == device:
return client.ip
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the router is up to date.
Return boolean if scanning successful.
"""
_LOGGER.info("Scanning")
if not self.success_init:
return False
with self.lock:
now = dt_util.now()
actiontec_data = self.get_actiontec_data()
if not actiontec_data:
return False
self.last_results = [Device(data['mac'], name, now)
for name, data in actiontec_data.items()
if data['timevalid'] > -60]
_LOGGER.info("actiontec scan successful")
return True
def get_actiontec_data(self):
"""Retrieve data from Actiontec MI424WR and return parsed result."""
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'Username: ')
telnet.write((self.username + '\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt = telnet.read_until(
b'Wireless Broadband Router> ').split(b'\n')[-1]
telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
telnet.write('\n'.encode('ascii'))
telnet.read_until(prompt)
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return None
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if match is not None:
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'timevalid': int(match.group('timevalid'))
}
return devices
@@ -12,14 +12,36 @@ import threading
from collections import namedtuple
from datetime import timedelta
from homeassistant.components.device_tracker import DOMAIN
import voluptuous as vol
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
CONF_PROTOCOL = 'protocol'
CONF_MODE = 'mode'
CONF_SSH_KEY = 'ssh_key'
CONF_PUB_KEY = 'pub_key'
PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_PASSWORD, CONF_PUB_KEY, CONF_SSH_KEY),
PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PROTOCOL, default='ssh'):
vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'):
vol.In(['router', 'ap']),
vol.Optional(CONF_SSH_KEY): cv.isfile,
vol.Optional(CONF_PUB_KEY): cv.isfile
}))
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1']
@@ -57,16 +79,6 @@ _IP_NEIGH_REGEX = re.compile(
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an ASUS-WRT scanner."""
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME]},
_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 private key or password must be provided')
return None
scanner = AsusWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
@@ -83,11 +95,11 @@ class AsusWrtDeviceScanner(object):
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME])
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')
self.username = config[CONF_USERNAME]
self.password = config.get(CONF_PASSWORD, '')
self.ssh_key = config.get('ssh_key', config.get('pub_key', ''))
self.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE]
self.lock = threading.Lock()
@@ -0,0 +1,108 @@
"""Tracking for bluetooth devices."""
import logging
from datetime import timedelta
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES,
CONF_TRACK_NEW,
CONF_SCAN_INTERVAL,
DEFAULT_SCAN_INTERVAL,
load_config,
)
import homeassistant.util as util
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['gattlib==0.20150805']
BLE_PREFIX = 'BLE_'
MIN_SEEN_NEW = 5
def setup_scanner(hass, config, see):
"""Setup the Bluetooth LE Scanner."""
# pylint: disable=import-error
from gattlib import DiscoveryService
new_devices = {}
def see_device(address, name, new_device=False):
"""Mark a device as seen."""
if new_device:
if address in new_devices:
_LOGGER.debug("Seen %s %s times", address,
new_devices[address])
new_devices[address] += 1
if new_devices[address] >= MIN_SEEN_NEW:
_LOGGER.debug("Adding %s to tracked devices", address)
devs_to_track.append(address)
else:
return
else:
_LOGGER.debug("Seen %s for the first time", address)
new_devices[address] = 1
return
see(mac=BLE_PREFIX + address, host_name=name.strip("\x00"))
def discover_ble_devices():
"""Discover Bluetooth LE devices."""
_LOGGER.debug("Discovering Bluetooth LE devices")
service = DiscoveryService()
devices = service.discover(10)
_LOGGER.debug("Bluetooth LE devices discovered = %s", devices)
return devices
yaml_path = hass.config.path(YAML_DEVICES)
devs_to_track = []
devs_donot_track = []
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0):
# check if device is a valid bluetooth device
if device.mac and device.mac[:3].upper() == BLE_PREFIX:
if device.track:
devs_to_track.append(device.mac[3:])
else:
devs_donot_track.append(device.mac[3:])
# if track new devices is true discover new devices
# on every scan.
track_new = util.convert(config.get(CONF_TRACK_NEW), bool,
len(devs_to_track) == 0)
if not devs_to_track and not track_new:
_LOGGER.warning("No Bluetooth LE devices to track!")
return False
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
DEFAULT_SCAN_INTERVAL)
def update_ble(now):
"""Lookup Bluetooth LE devices and update status."""
devs = discover_ble_devices()
for mac in devs_to_track:
_LOGGER.debug("Checking " + mac)
result = mac in devs
if not result:
# Could not lookup device name
continue
see_device(mac, devs[mac])
if track_new:
for address in devs:
if address not in devs_to_track and \
address not in devs_donot_track:
_LOGGER.info("Discovered Bluetooth LE device %s", address)
see_device(address, devs[address], new_device=True)
track_point_in_utc_time(hass, update_ble,
now + timedelta(seconds=interval))
update_ble(dt_util.utcnow())
return True
@@ -45,7 +45,7 @@ def setup_scanner(hass, config, see):
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0, 0):
for device in load_config(yaml_path, hass, 0):
# check if device is a valid bluetooth device
if device.mac and device.mac[:3].upper() == BT_PREFIX:
if device.track:
@@ -5,17 +5,28 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.icloud/
"""
import logging
import re
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_START)
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.util import slugify
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
PLATFORM_SCHEMA)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyicloud==0.8.3']
REQUIREMENTS = ['pyicloud==0.9.1']
CONF_INTERVAL = 'interval'
DEFAULT_INTERVAL = 8
KEEPALIVE_INTERVAL = 4
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): vol.Coerce(str),
vol.Required(CONF_PASSWORD): vol.Coerce(str),
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
vol.Range(min=1))
})
def setup_scanner(hass, config, see):
@@ -23,63 +34,67 @@ def setup_scanner(hass, config, see):
from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException
from pyicloud.exceptions import PyiCloudNoDevicesException
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
# Get the username and password from the configuration.
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is None or password is None:
_LOGGER.error('Must specify a username and password')
return False
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
try:
_LOGGER.info('Logging into iCloud Account')
# Attempt the login to iCloud
api = PyiCloudService(username,
password,
verify=True)
api = PyiCloudService(username, password, verify=True)
except PyiCloudFailedLoginException as error:
_LOGGER.exception('Error logging into iCloud Service: %s', error)
return False
def keep_alive(now):
"""Keep authenticating iCloud connection."""
"""Keep authenticating iCloud connection.
The session timeouts if we are not using it so we
have to re-authenticate & this will send an email.
"""
api.authenticate()
_LOGGER.info("Authenticate against iCloud")
track_utc_time_change(hass, keep_alive, second=0)
seen_devices = {}
def update_icloud(now):
"""Authenticate against iCloud and scan for devices."""
try:
# The session timeouts if we are not using it so we
# have to re-authenticate. This will send an email.
api.authenticate()
keep_alive(None)
# Loop through every device registered with the iCloud account
for device in api.devices:
status = device.status()
dev_id = slugify(status['name'].replace(' ', '', 99))
# An entity will not be created by see() when track=false in
# 'known_devices.yaml', but we need to see() it at least once
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id))
if entity is None and dev_id in seen_devices:
continue
seen_devices[dev_id] = True
location = device.location()
# If the device has a location add it. If not do nothing
if location:
see(
dev_id=re.sub(r"(\s|\W|')",
'',
status['name']),
dev_id=dev_id,
host_name=status['name'],
gps=(location['latitude'], location['longitude']),
battery=status['batteryLevel']*100,
gps_accuracy=location['horizontalAccuracy']
)
else:
# No location found for the device so continue
continue
except PyiCloudNoDevicesException:
_LOGGER.info('No iCloud Devices found!')
track_utc_time_change(
hass, update_icloud,
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
second=0
)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
# Schedule keepalives between the updates
keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
if x not in update_minutes)
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
return True
@@ -21,10 +21,10 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
# interval in minutes to exclude devices from a scan while they are home
# Interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = "home_interval"
REQUIREMENTS = ['python-nmap==0.6.0']
REQUIREMENTS = ['python-nmap==0.6.1']
def get_scanner(hass, config):
@@ -29,6 +29,9 @@ LOCK = threading.Lock()
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
VALIDATE_LOCATION = 'location'
VALIDATE_TRANSITION = 'transition'
def setup_scanner(hass, config, see):
"""Setup an OwnTracks tracker."""
@@ -47,16 +50,18 @@ def setup_scanner(hass, config, see):
'because of missing or malformatted data: %s',
data_type, data)
return None
if data_type == VALIDATE_TRANSITION:
return data
if max_gps_accuracy is not None and \
convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
_LOGGER.debug('Skipping %s update because expected GPS '
'accuracy %s is not met: %s',
data_type, max_gps_accuracy, data)
_LOGGER.warning('Ignoring %s update because expected GPS '
'accuracy %s is not met: %s',
data_type, max_gps_accuracy, payload)
return None
if convert(data.get('acc'), float, 1.0) == 0.0:
_LOGGER.debug('Skipping %s update because GPS accuracy'
'is zero',
data_type)
_LOGGER.warning('Ignoring %s update because GPS accuracy'
'is zero: %s',
data_type, payload)
return None
return data
@@ -65,7 +70,7 @@ def setup_scanner(hass, config, see):
"""MQTT message received."""
# Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typelocation
data = validate_payload(payload, 'location')
data = validate_payload(payload, VALIDATE_LOCATION)
if not data:
return
@@ -86,7 +91,7 @@ def setup_scanner(hass, config, see):
"""MQTT event (geofences) received."""
# Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typetransition
data = validate_payload(payload, 'transition')
data = validate_payload(payload, VALIDATE_TRANSITION)
if not data:
return
@@ -143,14 +148,24 @@ def setup_scanner(hass, config, see):
else:
_LOGGER.info("Exit to GPS")
# Check for GPS accuracy
if not ('acc' in data and
max_gps_accuracy is not None and
data['acc'] > max_gps_accuracy):
valid_gps = True
if 'acc' in data:
if data['acc'] == 0.0:
valid_gps = False
_LOGGER.warning(
'Ignoring GPS in region exit because accuracy'
'is zero: %s',
payload)
if (max_gps_accuracy is not None and
data['acc'] > max_gps_accuracy):
valid_gps = False
_LOGGER.warning(
'Ignoring GPS in region exit because expected '
'GPS accuracy %s is not met: %s',
max_gps_accuracy, payload)
if valid_gps:
see(**kwargs)
see_beacons(dev_id, kwargs)
else:
_LOGGER.info("Inaccurate GPS reported")
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
if location in beacons:
@@ -313,19 +313,23 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/{}/userRpm/WlanStationRpm.htm' \
.format(self.host, self.token)
referer = 'http://{}'.format(self.host)
cookie = 'Authorization=Basic {}'.format(self.credentials)
mac_results = []
page = requests.get(url, headers={
'cookie': cookie,
'referer': referer
})
result = self.parse_macs.findall(page.text)
# Check both the 2.4GHz and 5GHz client list URLs
for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'):
url = 'http://{}/{}/userRpm/{}' \
.format(self.host, self.token, clients_url)
referer = 'http://{}'.format(self.host)
cookie = 'Authorization=Basic {}'.format(self.credentials)
if not result:
page = requests.get(url, headers={
'cookie': cookie,
'referer': referer
})
mac_results.extend(self.parse_macs.findall(page.text))
if not mac_results:
return False
self.last_results = [mac.replace("-", ":") for mac in result]
self.last_results = [mac.replace("-", ":") for mac in mac_results]
return True
+5 -4
View File
@@ -12,13 +12,13 @@ import threading
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.7']
REQUIREMENTS = ['netdisco==0.7.1']
DOMAIN = 'discovery'
SCAN_INTERVAL = 300 # seconds
SERVICE_WEMO = 'belkin_wemo'
SERVICE_NETGEAR = 'netgear_router'
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HANDLERS = {
SERVICE_NETGEAR: ('device_tracker', None),
@@ -30,6 +30,7 @@ SERVICE_HANDLERS = {
'roku': ('media_player', 'roku'),
'sonos': ('media_player', 'sonos'),
'logitech_mediaserver': ('media_player', 'squeezebox'),
'directv': ('media_player', 'directv'),
}
+11 -11
View File
@@ -16,20 +16,20 @@ from homeassistant.helpers import validate_config
import homeassistant.helpers.config_validation as cv
from homeassistant.util import sanitize_filename
DOMAIN = "downloader"
SERVICE_DOWNLOAD_FILE = "download_file"
ATTR_URL = "url"
ATTR_SUBDIR = "subdir"
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
vol.Required(ATTR_URL): vol.Url,
vol.Optional(ATTR_SUBDIR): cv.string,
})
ATTR_SUBDIR = 'subdir'
ATTR_URL = 'url'
CONF_DOWNLOAD_DIR = 'download_dir'
DOMAIN = 'downloader'
SERVICE_DOWNLOAD_FILE = 'download_file'
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_SUBDIR): cv.string,
})
# pylint: disable=too-many-branches
def setup(hass, config):
+9 -11
View File
@@ -6,30 +6,28 @@ https://home-assistant.io/components/dweet/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNKNOWN
from homeassistant.const import (
CONF_NAME, CONF_WHITELIST, EVENT_STATE_CHANGED, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper
from homeassistant.util import Throttle
REQUIREMENTS = ['dweepy==0.2.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "dweet"
DEPENDENCIES = []
REQUIREMENTS = ['dweepy==0.2.0']
CONF_NAME = 'name'
CONF_WHITELIST = 'whitelist'
DOMAIN = 'dweet'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_WHITELIST): cv.string,
vol.Required(CONF_WHITELIST, default=[]):
vol.All(cv.ensure_list, [cv.entity_id]),
}),
}, extra=vol.ALLOW_EXTRA)
@@ -38,8 +36,8 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config):
"""Setup the Dweet.io component."""
conf = config[DOMAIN]
name = conf[CONF_NAME]
whitelist = conf.get(CONF_WHITELIST, [])
name = conf.get(CONF_NAME)
whitelist = conf.get(CONF_WHITELIST)
json_body = {}
def dweet_event_listener(event):
+13 -3
View File
@@ -7,7 +7,9 @@ https://home-assistant.io/components/ecobee/
import logging
import os
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY
from homeassistant.loader import get_component
@@ -15,12 +17,19 @@ from homeassistant.util import Throttle
DOMAIN = "ecobee"
NETWORK = None
HOLD_TEMP = 'hold_temp'
CONF_HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/'
'4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_API_KEY): cv.string,
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean
})
}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
ECOBEE_CONFIG_FILE = 'ecobee.conf'
@@ -67,11 +76,12 @@ def setup_ecobee(hass, network, config):
configurator = get_component('configurator')
configurator.request_done(_CONFIGURING.pop('ecobee'))
hold_temp = config[DOMAIN].get(HOLD_TEMP, False)
hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP)
discovery.load_platform(hass, 'thermostat', DOMAIN,
discovery.load_platform(hass, 'climate', DOMAIN,
{'hold_temp': hold_temp}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
# pylint: disable=too-few-public-methods
+542
View File
@@ -0,0 +1,542 @@
"""
Support for local control of entities by emulating the Phillips Hue bridge.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/
"""
import threading
import socket
import logging
import json
import os
import select
import voluptuous as vol
from homeassistant import util, core
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
STATE_ON
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
)
from homeassistant.components.http import (
HomeAssistantView, HomeAssistantWSGI
)
import homeassistant.helpers.config_validation as cv
DOMAIN = 'emulated_hue'
_LOGGER = logging.getLogger(__name__)
CONF_HOST_IP = 'host_ip'
CONF_LISTEN_PORT = 'listen_port'
CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
CONF_EXPOSED_DOMAINS = 'exposed_domains'
ATTR_EMULATED_HUE = 'emulated_hue'
ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
DEFAULT_LISTEN_PORT = 8300
DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
'switch', 'light', 'group', 'input_boolean', 'media_player'
]
HUE_API_STATE_ON = 'on'
HUE_API_STATE_BRI = 'bri'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST_IP): cv.string,
vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list,
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, yaml_config):
"""Activate the emulated_hue component."""
config = Config(yaml_config)
server = HomeAssistantWSGI(
hass,
development=False,
server_host=config.host_ip_addr,
server_port=config.listen_port,
api_password=None,
ssl_certificate=None,
ssl_key=None,
cors_origins=[]
)
server.register_view(DescriptionXmlView(hass, config))
server.register_view(HueUsernameView(hass))
server.register_view(HueLightsView(hass, config))
upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port)
def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge."""
server.start()
upnp_listener.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge."""
upnp_listener.stop()
server.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)
return True
# pylint: disable=too-few-public-methods
class Config(object):
"""Holds configuration variables for the emulated hue bridge."""
def __init__(self, yaml_config):
"""Initialize the instance."""
conf = yaml_config.get(DOMAIN, {})
# Get the IP address that will be passed to the Echo during discovery
self.host_ip_addr = conf.get(CONF_HOST_IP)
if self.host_ip_addr is None:
self.host_ip_addr = util.get_local_ip()
_LOGGER.warning(
"Listen IP address not specified, auto-detected address is %s",
self.host_ip_addr)
# Get the port that the Hue bridge will listen on
self.listen_port = conf.get(CONF_LISTEN_PORT)
if not isinstance(self.listen_port, int):
self.listen_port = DEFAULT_LISTEN_PORT
_LOGGER.warning(
"Listen port not specified, defaulting to %s",
self.listen_port)
# Get domains that cause both "on" and "off" commands to map to "on"
# This is primarily useful for things like scenes or scripts, which
# don't really have a concept of being off
self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS)
if not isinstance(self.off_maps_to_on_domains, list):
self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS
# Get whether or not entities should be exposed by default, or if only
# explicitly marked ones will be exposed
self.expose_by_default = conf.get(
CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT)
# Get domains that are exposed by default when expose_by_default is
# True
self.exposed_domains = conf.get(
CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
class DescriptionXmlView(HomeAssistantView):
"""Handles requests for the description.xml file."""
url = '/description.xml'
name = 'description:xml'
requires_auth = False
def __init__(self, hass, config):
"""Initialize the instance of the view."""
super().__init__(hass)
self.config = config
def get(self, request):
"""Handle a GET request."""
xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://{0}:{1}/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>HASS Bridge ({0})</friendlyName>
<manufacturer>Royal Philips Electronics</manufacturer>
<manufacturerURL>http://www.philips.com</manufacturerURL>
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
<modelName>Philips hue bridge 2015</modelName>
<modelNumber>BSB002</modelNumber>
<modelURL>http://www.meethue.com</modelURL>
<serialNumber>1234</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-001788255acc</UDN>
</device>
</root>
"""
resp_text = xml_template.format(
self.config.host_ip_addr, self.config.listen_port)
return self.Response(resp_text, mimetype='text/xml')
class HueUsernameView(HomeAssistantView):
"""Handle requests to create a username for the emulated hue bridge."""
url = '/api'
name = 'hue:api'
requires_auth = False
def __init__(self, hass):
"""Initialize the instance of the view."""
super().__init__(hass)
def post(self, request):
"""Handle a POST request."""
data = request.json
if 'devicetype' not in data:
return self.Response("devicetype not specified", status=400)
json_response = [{'success': {'username': '12345678901234567890'}}]
return self.json(json_response)
class HueLightsView(HomeAssistantView):
"""Handle requests for getting and setting info about entities."""
url = '/api/<username>/lights'
name = 'api:username:lights'
extra_urls = ['/api/<username>/lights/<entity_id>',
'/api/<username>/lights/<entity_id>/state']
requires_auth = False
def __init__(self, hass, config):
"""Initialize the instance of the view."""
super().__init__(hass)
self.config = config
self.cached_states = {}
def get(self, request, username, entity_id=None):
"""Handle a GET request."""
if entity_id is None:
return self.get_lights_list()
if not request.base_url.endswith('state'):
return self.get_light_state(entity_id)
return self.Response("Method not allowed", status=405)
def put(self, request, username, entity_id=None):
"""Handle a PUT request."""
if not request.base_url.endswith('state'):
return self.Response("Method not allowed", status=405)
content_type = request.environ.get('CONTENT_TYPE', '')
if content_type == 'application/x-www-form-urlencoded':
# Alexa sends JSON data with a form data content type, for
# whatever reason, and Werkzeug parses form data automatically,
# so we need to do some gymnastics to get the data we need
json_data = None
for key in request.form:
try:
json_data = json.loads(key)
break
except ValueError:
# Try the next key?
pass
if json_data is None:
return self.Response("Bad request", status=400)
else:
json_data = request.json
return self.put_light_state(json_data, entity_id)
def get_lights_list(self):
"""Process a request to get the list of available lights."""
json_response = {}
for entity in self.hass.states.all():
if self.is_entity_exposed(entity):
json_response[entity.entity_id] = entity_to_json(entity)
return self.json(json_response)
def get_light_state(self, entity_id):
"""Process a request to get the state of an individual light."""
entity = self.hass.states.get(entity_id)
if entity is None or not self.is_entity_exposed(entity):
return self.Response("Entity not found", status=404)
cached_state = self.cached_states.get(entity_id, None)
if cached_state is None:
final_state = entity.state == STATE_ON
final_brightness = entity.attributes.get(
ATTR_BRIGHTNESS, 255 if final_state else 0)
else:
final_state, final_brightness = cached_state
json_response = entity_to_json(entity, final_state, final_brightness)
return self.json(json_response)
def put_light_state(self, request_json, entity_id):
"""Process a request to set the state of an individual light."""
config = self.config
# Retrieve the entity from the state machine
entity = self.hass.states.get(entity_id)
if entity is None:
return self.Response("Entity not found", status=404)
if not self.is_entity_exposed(entity):
return self.Response("Entity not found", status=404)
# Parse the request into requested "on" status and brightness
parsed = parse_hue_api_put_light_body(request_json, entity)
if parsed is None:
return self.Response("Bad request", status=400)
result, brightness = parsed
# Convert the resulting "on" status into the service we need to call
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
# Construct what we need to send to the service
data = {ATTR_ENTITY_ID: entity_id}
if brightness is not None:
data[ATTR_BRIGHTNESS] = brightness
if entity.domain.lower() in config.off_maps_to_on_domains:
# Map the off command to on
service = SERVICE_TURN_ON
# Caching is required because things like scripts and scenes won't
# report as "off" to Alexa if an "off" command is received, because
# they'll map to "on". Thus, instead of reporting its actual
# status, we report what Alexa will want to see, which is the same
# as the actual requested command.
self.cached_states[entity_id] = (result, brightness)
# Perform the requested action
self.hass.services.call(core.DOMAIN, service, data, blocking=True)
json_response = \
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
if brightness is not None:
json_response.append(create_hue_success_response(
entity_id, HUE_API_STATE_BRI, brightness))
return self.json(json_response)
def is_entity_exposed(self, entity):
"""Determine if an entity should be exposed on the emulated bridge."""
config = self.config
if entity.attributes.get('view') is not None:
# Ignore entities that are views
return False
domain = entity.domain.lower()
explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
domain_exposed_by_default = \
config.expose_by_default and domain in config.exposed_domains
# Expose an entity if the entity's domain is exposed by default and
# the configuration doesn't explicitly exclude it from being
# exposed, or if the entity is explicitly exposed
is_default_exposed = \
domain_exposed_by_default and explicit_expose is not False
return is_default_exposed or explicit_expose
def parse_hue_api_put_light_body(request_json, entity):
"""Parse the body of a request to change the state of a light."""
if HUE_API_STATE_ON in request_json:
if not isinstance(request_json[HUE_API_STATE_ON], bool):
return None
if request_json['on']:
# Echo requested device be turned on
brightness = None
report_brightness = False
result = True
else:
# Echo requested device be turned off
brightness = None
report_brightness = False
result = False
if HUE_API_STATE_BRI in request_json:
# Make sure the entity actually supports brightness
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
try:
# Clamp brightness from 0 to 255
brightness = \
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
except ValueError:
return None
report_brightness = True
result = (brightness > 0)
return (result, brightness) if report_brightness else (result, None)
def entity_to_json(entity, is_on=None, brightness=None):
"""Convert an entity to its Hue bridge JSON representation."""
if is_on is None:
is_on = entity.state == STATE_ON
if brightness is None:
brightness = 255 if is_on else 0
name = entity.attributes.get(
ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
return {
'state':
{
HUE_API_STATE_ON: is_on,
HUE_API_STATE_BRI: brightness,
'reachable': True
},
'type': 'Dimmable light',
'name': name,
'modelid': 'HASS123',
'uniqueid': entity.entity_id,
'swversion': '123'
}
def create_hue_success_response(entity_id, attr, value):
"""Create a success response for an attribute set on a light."""
success_key = '/lights/{}/state/{}'.format(entity_id, attr)
return {'success': {success_key: value}}
class UPNPResponderThread(threading.Thread):
"""Handle responding to UPNP/SSDP discovery requests."""
_interrupted = False
def __init__(self, host_ip_addr, listen_port):
"""Initialize the class."""
threading.Thread.__init__(self)
self.host_ip_addr = host_ip_addr
self.listen_port = listen_port
# Note that the double newline at the end of
# this string is required per the SSDP spec
resp_template = """HTTP/1.1 200 OK
CACHE-CONTROL: max-age=60
EXT:
LOCATION: http://{0}:{1}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
ST: urn:schemas-upnp-org:device:basic:1
USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
"""
self.upnp_response = resp_template.format(host_ip_addr, listen_port) \
.replace("\n", "\r\n") \
.encode('utf-8')
# Set up a pipe for signaling to the receiver that it's time to
# shutdown. Essentially, we place the SSDP socket into nonblocking
# mode and use select() to wait for data to arrive on either the SSDP
# socket or the pipe. If data arrives on either one, select() returns
# and tells us which filenos have data ready to read.
#
# When we want to stop the responder, we write data to the pipe, which
# causes the select() to return and indicate that said pipe has data
# ready to be read, which indicates to us that the responder needs to
# be shutdown.
self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe()
def run(self):
"""Run the server."""
# Listen for UDP port 1900 packets sent to SSDP multicast address
ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ssdp_socket.setblocking(False)
# Required for receiving multicast
ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ssdp_socket.setsockopt(
socket.SOL_IP,
socket.IP_MULTICAST_IF,
socket.inet_aton(self.host_ip_addr))
ssdp_socket.setsockopt(
socket.SOL_IP,
socket.IP_ADD_MEMBERSHIP,
socket.inet_aton("239.255.255.250") +
socket.inet_aton(self.host_ip_addr))
ssdp_socket.bind(("239.255.255.250", 1900))
while True:
if self._interrupted:
clean_socket_close(ssdp_socket)
return
try:
read, _, _ = select.select(
[self._interrupted_read_pipe, ssdp_socket], [],
[ssdp_socket])
if self._interrupted_read_pipe in read:
# Implies self._interrupted is True
clean_socket_close(ssdp_socket)
return
elif ssdp_socket in read:
data, addr = ssdp_socket.recvfrom(1024)
else:
continue
except socket.error as ex:
if self._interrupted:
clean_socket_close(ssdp_socket)
return
_LOGGER.error("UPNP Responder socket exception occured: %s",
ex.__str__)
if "M-SEARCH" in data.decode('utf-8'):
# SSDP M-SEARCH method received, respond to it with our info
resp_socket = socket.socket(
socket.AF_INET, socket.SOCK_DGRAM)
resp_socket.sendto(self.upnp_response, addr)
resp_socket.close()
def stop(self):
"""Stop the server."""
# Request for server
self._interrupted = True
os.write(self._interrupted_write_pipe, bytes([0]))
self.join()
def clean_socket_close(sock):
"""Close a socket connection and logs its closure."""
_LOGGER.info("UPNP responder shutting down.")
sock.close()
+16 -3
View File
@@ -4,23 +4,36 @@ EnOcean Component.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/EnOcean/
"""
import logging
DOMAIN = "enocean"
import voluptuous as vol
from homeassistant.const import CONF_DEVICE
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['enocean==0.31']
CONF_DEVICE = "device"
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'enocean'
ENOCEAN_DONGLE = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup the EnOcean component."""
global ENOCEAN_DONGLE
serial_dev = config[DOMAIN].get(CONF_DEVICE, "/dev/ttyUSB0")
serial_dev = config[DOMAIN].get(CONF_DEVICE)
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev)
return True
+1 -2
View File
@@ -62,8 +62,7 @@ CONFIG_SCHEMA = vol.Schema({
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_PORT, default=DEFAULT_PORT): cv.port,
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):
+247
View File
@@ -0,0 +1,247 @@
"""
Provides functionality to interact with fans.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan/
"""
import logging
import os
import voluptuous as vol
from homeassistant.components import group
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE,
SERVICE_TURN_OFF, ATTR_ENTITY_ID,
STATE_UNKNOWN)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
DOMAIN = 'fan'
SCAN_INTERVAL = 30
GROUP_NAME_ALL_FANS = 'all fans'
ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Bitfield of features supported by the fan entity
ATTR_SUPPORTED_FEATURES = 'supported_features'
SUPPORT_SET_SPEED = 1
SUPPORT_OSCILLATE = 2
SERVICE_SET_SPEED = 'set_speed'
SERVICE_OSCILLATE = 'oscillate'
SPEED_OFF = 'off'
SPEED_LOW = 'low'
SPEED_MED = 'med'
SPEED_HIGH = 'high'
ATTR_SPEED = 'speed'
ATTR_SPEED_LIST = 'speed_list'
ATTR_OSCILLATING = 'oscillating'
PROP_TO_ATTR = {
'speed': ATTR_SPEED,
'speed_list': ATTR_SPEED_LIST,
'oscillating': ATTR_OSCILLATING,
'supported_features': ATTR_SUPPORTED_FEATURES,
} # type: dict
FAN_SET_SPEED_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_SPEED): cv.string
}) # type: dict
FAN_TURN_ON_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_SPEED): cv.string
}) # type: dict
FAN_TURN_OFF_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids
}) # type: dict
FAN_OSCILLATE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_OSCILLATING): cv.boolean
}) # type: dict
FAN_TOGGLE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids
})
_LOGGER = logging.getLogger(__name__)
def is_on(hass, entity_id: str=None) -> bool:
"""Return if the fans are on based on the statemachine."""
entity_id = entity_id or ENTITY_ID_ALL_FANS
state = hass.states.get(entity_id)
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN]
# pylint: disable=too-many-arguments
def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
"""Turn all or specified fan on."""
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_SPEED, speed),
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id: str=None) -> None:
"""Turn all or specified fan off."""
data = {
ATTR_ENTITY_ID: entity_id,
}
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
def toggle(hass, entity_id: str=None) -> None:
"""Toggle all or specified fans."""
data = {
ATTR_ENTITY_ID: entity_id
}
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
def oscillate(hass, entity_id: str=None, should_oscillate: bool=True) -> None:
"""Set oscillation on all or specified fan."""
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_OSCILLATING, should_oscillate),
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_OSCILLATE, data)
def set_speed(hass, entity_id: str=None, speed: str=None) -> None:
"""Set speed for all or specified fan."""
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_SPEED, speed),
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_SET_SPEED, data)
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
def setup(hass, config: dict) -> None:
"""Expose fan control via statemachine and services."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS)
component.setup(config)
def handle_fan_service(service: str) -> None:
"""Hande service call for fans."""
# Get the validated data
params = service.data.copy()
# Convert the entity ids to valid fan ids
target_fans = component.extract_from_service(service)
params.pop(ATTR_ENTITY_ID, None)
service_fun = None
for service_def in [SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_SET_SPEED, SERVICE_OSCILLATE]:
if service_def == service.service:
service_fun = service_def
break
if service_fun:
for fan in target_fans:
getattr(fan, service_fun)(**params)
for fan in target_fans:
if fan.should_poll:
fan.update_ha_state(True)
return
# Listen for fan service calls.
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_fan_service,
descriptions.get(SERVICE_TURN_ON),
schema=FAN_TURN_ON_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_fan_service,
descriptions.get(SERVICE_TURN_OFF),
schema=FAN_TURN_OFF_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_SPEED, handle_fan_service,
descriptions.get(SERVICE_SET_SPEED),
schema=FAN_SET_SPEED_SCHEMA)
hass.services.register(DOMAIN, SERVICE_OSCILLATE, handle_fan_service,
descriptions.get(SERVICE_OSCILLATE),
schema=FAN_OSCILLATE_SCHEMA)
return True
class FanEntity(ToggleEntity):
"""Representation of a fan."""
# pylint: disable=no-self-use, abstract-method
def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan."""
pass
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
raise NotImplementedError()
def turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn off the fan."""
raise NotImplementedError()
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
"""Oscillate the fan."""
pass
@property
def is_on(self):
"""Return true if the entity is on."""
return self.state_attributes.get(ATTR_SPEED, STATE_UNKNOWN) \
not in [SPEED_OFF, STATE_UNKNOWN]
@property
def speed_list(self: ToggleEntity) -> list:
"""Get the list of available speeds."""
return []
@property
def state_attributes(self: ToggleEntity) -> dict:
"""Return optional state attributes."""
data = {} # type: dict
for prop, attr in PROP_TO_ATTR.items():
if not hasattr(self, prop):
continue
value = getattr(self, prop)
if value is not None:
data[attr] = value
return data
@property
def supported_features(self: ToggleEntity) -> int:
"""Flag supported features."""
return 0
+75
View File
@@ -0,0 +1,75 @@
"""
Demo garage door platform that has a fake fan.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH,
FanEntity, SUPPORT_SET_SPEED,
SUPPORT_OSCILLATE)
from homeassistant.const import STATE_OFF
FAN_NAME = 'Living Room Fan'
FAN_ENTITY_ID = 'fan.living_room_fan'
DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup demo garage door platform."""
add_devices_callback([
DemoFan(hass, FAN_NAME, STATE_OFF),
])
class DemoFan(FanEntity):
"""A demonstration fan component."""
def __init__(self, hass, name: str, initial_state: str) -> None:
"""Initialize the entity."""
self.hass = hass
self.speed = initial_state
self.oscillating = False
self._name = name
@property
def name(self) -> str:
"""Get entity name."""
return self._name
@property
def should_poll(self):
"""No polling needed for a demo fan."""
return False
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]
def turn_on(self, speed: str=SPEED_MED) -> None:
"""Turn on the entity."""
self.set_speed(speed)
def turn_off(self) -> None:
"""Turn off the entity."""
self.oscillate(False)
self.set_speed(STATE_OFF)
def set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
self.speed = speed
self.update_ha_state()
def oscillate(self, oscillating: bool) -> None:
"""Set oscillation."""
self.oscillating = oscillating
self.update_ha_state()
@property
def supported_features(self) -> int:
"""Flag supported features."""
return DEMO_SUPPORT
@@ -0,0 +1,66 @@
"""
Support for Insteon FanLinc.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.insteon/
"""
import logging
from homeassistant.components.fan import (FanEntity, SUPPORT_SET_SPEED,
SPEED_OFF, SPEED_LOW, SPEED_MED,
SPEED_HIGH)
from homeassistant.components.insteon_hub import (InsteonDevice, INSTEON,
filter_devices)
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
DEVICE_CATEGORIES = [
{
'DevCat': 1,
'SubCat': [46]
}
]
DEPENDENCIES = ['insteon_hub']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Insteon Hub fan platform."""
devs = []
for device in filter_devices(INSTEON.devices, DEVICE_CATEGORIES):
devs.append(InsteonFanDevice(device))
add_devices(devs)
class InsteonFanDevice(InsteonDevice, FanEntity):
"""Represet an insteon fan device."""
def __init__(self, node: object) -> None:
"""Initialize the device."""
super(InsteonFanDevice, self).__init__(node)
self.speed = STATE_UNKNOWN # Insteon hub can't get state via REST
def turn_on(self, speed: str=None):
"""Turn the fan on."""
self.set_speed(speed if speed else SPEED_MED)
def turn_off(self):
"""Turn the fan off."""
self.set_speed(SPEED_OFF)
def set_speed(self, speed: str) -> None:
"""Set the fan speed."""
if self._send_command('fan', payload={'speed', speed}):
self.speed = speed
@property
def supported_features(self) -> int:
"""Get the supported features for device."""
return SUPPORT_SET_SPEED
@property
def speed_list(self) -> list:
"""Get the available speeds for the fan."""
return [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]
@@ -0,0 +1,53 @@
# Describes the format for available fan services
set_speed:
description: Sets fan speed
fields:
entity_id:
description: Name(s) of the entities to set
example: 'fan.living_room'
speed:
description: Speed setting
example: 'low'
turn_on:
description: Turns fan on
fields:
entity_id:
description: Names(s) of the entities to turn on
example: 'fan.living_room'
speed:
description: Speed setting
example: 'high'
turn_off:
description: Turns fan off
fields:
entity_id:
description: Names(s) of the entities to turn off
example: 'fan.living_room'
oscillate:
description: Oscillates the fan
fields:
entity_id:
description: Name(s) of the entities to oscillate
example: 'fan.desk_fan'
oscillating:
description: Flag to turn on/off oscillation
example: True
toggle:
description: Toggle the fan on/off
fields:
entity_id:
description: Name(s) of the entities to toggle
exampl: 'fan.living_room'
+99
View File
@@ -0,0 +1,99 @@
"""
Allows utilizing the Foursquare (Swarm) API.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/foursquare/
"""
import logging
import os
import json
import requests
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
DOMAIN = "foursquare"
SERVICE_CHECKIN = "checkin"
EVENT_PUSH = "foursquare.push"
EVENT_CHECKIN = "foursquare.checkin"
CHECKIN_SERVICE_SCHEMA = vol.Schema({
vol.Required("venueId"): cv.string,
vol.Optional("eventId"): cv.string,
vol.Optional("shout"): cv.string,
vol.Optional("mentions"): cv.string,
vol.Optional("broadcast"): cv.string,
vol.Optional("ll"): cv.string,
vol.Optional("llAcc"): cv.string,
vol.Optional("alt"): cv.string,
vol.Optional("altAcc"): cv.string,
})
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["http"]
def setup(hass, config):
"""Setup the Foursquare component."""
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), "services.yaml"))
config = config[DOMAIN]
def checkin_user(call):
"""Check a user in on Swarm."""
url = ("https://api.foursquare.com/v2/checkins/add"
"?oauth_token={}"
"&v=20160802"
"&m=swarm").format(config["access_token"])
response = requests.post(url, data=call.data, timeout=10)
if response.status_code not in (200, 201):
_LOGGER.exception(
"Error checking in user. Response %d: %s:",
response.status_code, response.reason)
hass.bus.fire(EVENT_CHECKIN, response.text)
# Register our service with Home Assistant.
hass.services.register(DOMAIN, "checkin", checkin_user,
descriptions[DOMAIN][SERVICE_CHECKIN],
schema=CHECKIN_SERVICE_SCHEMA)
hass.wsgi.register_view(FoursquarePushReceiver(hass,
config["push_secret"]))
return True
class FoursquarePushReceiver(HomeAssistantView):
"""Handle pushes from the Foursquare API."""
requires_auth = False
url = "/api/foursquare"
name = "foursquare"
def __init__(self, hass, push_secret):
"""Initialize the OAuth callback view."""
super().__init__(hass)
self.push_secret = push_secret
def post(self, request):
"""Accept the POST from Foursquare."""
raw_data = request.form
_LOGGER.debug("Received Foursquare push: %s", raw_data)
if self.push_secret != raw_data["secret"]:
_LOGGER.error("Received Foursquare push with invalid"
"push secret! Data: %s", raw_data)
return
parsed_payload = {
key: json.loads(val) for key, val in raw_data.items()
if key != "secret"
}
self.hass.bus.fire(EVENT_PUSH, parsed_payload)
+168 -34
View File
@@ -1,37 +1,157 @@
"""Handle the frontend for Home Assistant."""
import hashlib
import logging
import os
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView
from . import version, mdi_version
from .version import FINGERPRINTS
DOMAIN = 'frontend'
DEPENDENCIES = ['api']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
PANELS = {}
MANIFEST_JSON = {
"background_color": "#FFFFFF",
"description": "Open-source home automation platform running on Python 3.",
"dir": "ltr",
"display": "standalone",
"icons": [],
"lang": "en-US",
"name": "Home Assistant",
"orientation": "any",
"short_name": "Assistant",
"start_url": "/",
"theme_color": "#03A9F4"
}
# To keep track we don't register a component twice (gives a warning)
_REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__)
def register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, url_path=None, config=None):
"""Register a built-in panel."""
# pylint: disable=too-many-arguments
path = 'panels/ha-panel-{}.html'.format(component_name)
if hass.wsgi.development:
url = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name))
else:
url = None # use default url generate mechanism
register_panel(hass, component_name, os.path.join(STATIC_PATH, path),
FINGERPRINTS[path], sidebar_title, sidebar_icon, url_path,
url, config)
def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
sidebar_icon=None, url_path=None, url=None, config=None):
"""Register a panel for the frontend.
component_name: name of the web component
path: path to the HTML of the web component
md5: the md5 hash of the web component (for versioning, optional)
sidebar_title: title to show in the sidebar (optional)
sidebar_icon: icon to show next to title in sidebar (optional)
url_path: name to use in the url (defaults to component_name)
url: for the web component (for dev environment, optional)
config: config to be passed into the web component
Warning: this API will probably change. Use at own risk.
"""
# pylint: disable=too-many-arguments
if url_path is None:
url_path = component_name
if url_path in PANELS:
_LOGGER.warning('Overwriting component %s', url_path)
if not os.path.isfile(path):
_LOGGER.error('Panel %s component does not exist: %s',
component_name, path)
return
if md5 is None:
with open(path) as fil:
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
data = {
'url_path': url_path,
'component_name': component_name,
}
if sidebar_title:
data['title'] = sidebar_title
if sidebar_icon:
data['icon'] = sidebar_icon
if config is not None:
data['config'] = config
if url is not None:
data['url'] = url
else:
url = URL_PANEL_COMPONENT.format(component_name)
if url not in _REGISTERED_COMPONENTS:
hass.wsgi.register_static_path(url, path)
_REGISTERED_COMPONENTS.add(url)
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
data['url'] = fprinted_url
PANELS[url_path] = data
def add_manifest_json_key(key, val):
"""Add a keyval to the manifest.json."""
MANIFEST_JSON[key] = val
def setup(hass, config):
"""Setup serving the frontend."""
hass.wsgi.register_view(IndexView)
hass.wsgi.register_view(BootstrapView)
hass.wsgi.register_view(ManifestJSONView)
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
if hass.wsgi.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
sw_path = "service_worker.js"
hass.wsgi.register_static_path(
"/service_worker.js",
os.path.join(www_static_path, sw_path),
0
)
hass.wsgi.register_static_path(
"/robots.txt",
os.path.join(www_static_path, "robots.txt")
)
hass.wsgi.register_static_path("/static", www_static_path)
hass.wsgi.register_static_path("/service_worker.js",
os.path.join(STATIC_PATH, sw_path), 0)
hass.wsgi.register_static_path("/robots.txt",
os.path.join(STATIC_PATH, "robots.txt"))
hass.wsgi.register_static_path("/static", STATIC_PATH)
hass.wsgi.register_static_path("/local", hass.config.path('www'))
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template'):
register_built_in_panel(hass, panel)
def register_frontend_index(event):
"""Register the frontend index urls.
Done when Home Assistant is started so that all panels are known.
"""
hass.wsgi.register_view(IndexView(
hass, ['/{}'.format(name) for name in PANELS]))
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
for size in (192, 384, 512, 1024):
MANIFEST_JSON['icons'].append({
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
"sizes": "{}x{}".format(size, size),
"type": "image/png"
})
return True
@@ -48,6 +168,7 @@ class BootstrapView(HomeAssistantView):
'states': self.hass.states.all(),
'events': api.events_json(self.hass),
'services': api.services_json(self.hass),
'panels': PANELS,
})
@@ -57,16 +178,15 @@ class IndexView(HomeAssistantView):
url = '/'
name = "frontend:index"
requires_auth = False
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent', '/devInfo', '/devTemplate',
'/states', '/states/<entity:entity_id>']
extra_urls = ['/states', '/states/<entity:entity_id>']
def __init__(self, hass):
def __init__(self, hass, extra_urls):
"""Initialize the frontend view."""
super().__init__(hass)
from jinja2 import FileSystemLoader, Environment
self.extra_urls = self.extra_urls + extra_urls
self.templates = Environment(
loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/')
@@ -76,32 +196,46 @@ class IndexView(HomeAssistantView):
def get(self, request, entity_id=None):
"""Serve the index view."""
if self.hass.wsgi.development:
core_url = '/static/home-assistant-polymer/build/_core_compiled.js'
core_url = '/static/home-assistant-polymer/build/core.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 = '/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)
core_url = '/static/core-{}.js'.format(
FINGERPRINTS['core.js'])
ui_url = '/static/frontend-{}.html'.format(
FINGERPRINTS['frontend.html'])
if request.path == '/':
panel = 'states'
else:
panel = request.path.split('/')[1]
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
# auto login if no password was set
if self.hass.config.api.api_password is None:
auth = 'true'
else:
auth = 'false'
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
no_auth = 'false' if self.hass.config.api.api_password else 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
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, map_url=map_url, auth=auth,
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION)
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url, panels=PANELS)
return self.Response(resp, mimetype='text/html')
class ManifestJSONView(HomeAssistantView):
"""View to return a manifest.json."""
requires_auth = False
url = "/manifest.json"
name = "manifestjson"
def get(self, request):
"""Return the manifest.json."""
import json
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
return self.Response(msg, mimetype="application/manifest+json")
@@ -1,2 +0,0 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "758957b7ea989d6beca60e218ea7f7dd"
@@ -4,20 +4,30 @@
<meta charset="utf-8">
<title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json'>
<link rel='icon' href='/static/favicon.ico'>
<link rel='manifest' href='/manifest.json'>
<link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/favicon-apple-180x180.png'>
href='/static/icons/favicon-apple-180x180.png'>
{% for panel in panels.values() -%}
<link rel='prefetch' href='{{ panel.url }}'>
{% endfor -%}
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/>
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='#03a9f4'>
<style>
body {
font-family: 'Roboto', 'Noto', sans-serif;
font-weight: 300;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
#ha-init-skeleton {
display: -webkit-flex;
display: flex;
@@ -65,23 +75,23 @@
.getElementById('ha-init-skeleton')
.classList.add('error');
};
window.noAuth = {{ auth }};
window.deferredLoading = {
map: '{{ map_url }}',
dev: '{{ dev_url }}',
};
window.noAuth = {{ no_auth }};
window.Polymer = {lazyRegister: true, useNativeCSSProperties: true, dom: 'shady'};
</script>
</head>
<body fullbleed>
<body>
<div id='ha-init-skeleton'>
<img src='/static/favicon-192x192.png' height='192'>
<img src='/static/icons/favicon-192x192.png' height='192'>
<paper-spinner active></paper-spinner>
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
</div>
<home-assistant icons='{{ icons }}'></home-assistant>
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<script src='{{ core_url }}'></script>
<link rel='import' href='{{ ui_url }}' onerror='initError()' async>
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url -%}
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
{% endif -%}
<link rel='import' href='{{ icons_url }}' async>
<script>
var webComponentsSupported = (
@@ -89,11 +99,11 @@
'import' in document.createElement('link') &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
var script = document.createElement('script')
script.async = true
script.onerror = initError;
script.src = '/static/webcomponents-lite.min.js'
document.head.appendChild(script)
var e = document.createElement('script');
e.async = true;
e.onerror = initError;
e.src = '/static/webcomponents-lite.min.js';
document.head.appendChild(e);
}
</script>
</body>
+16 -5
View File
@@ -1,5 +1,16 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
UI = "805f8dda70419b26daabc8e8f625127f"
MAP = "c922306de24140afd14f857f927bf8f0"
DEV = "b7079ac3121b95b9856e5603a6d8a263"
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = {
"core.js": "1fd10c1fcdf56a61f60cf861d5a0368c",
"frontend.html": "88c97d278de3320278da6c32fe9e7d61",
"mdi.html": "710b84acc99b32514f52291aba9cd8e8",
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
"panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa",
"panels/ha-panel-dev-state.html": "4608326978256644c42b13940c028e0a",
"panels/ha-panel-dev-template.html": "0a099d4589636ed3038a3e9f020468a7",
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
"panels/ha-panel-map.html": "af7d04aff7dd5479c5a0016bc8d4dd7d"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

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