Compare commits

..

115 Commits

Author SHA1 Message Date
Fabian Affolter 79240a0d47 Merge pull request #11153 from home-assistant/release-0-60
0.60
2017-12-17 15:44:41 +01:00
Brad Dixon c03d04d826 Revbump to SoCo 0.13 and add support for Night Sound and Speech Enhancement. (#10765)
Sonos Playbar and Playbase devices support Night Sound and Speech Enhancement
effects when playing from sources such as a TV. Adds a new service "sonos_set_option"
whichs accepts boolean options to control these audio features.
2017-12-17 13:09:48 +01:00
Mike Megally 432304be82 Remove logging (#11173)
An error was being log that seems more like debug info
2017-12-17 13:07:23 +01:00
PhracturedBlue 294d8171a2 convert alarmdecoder interface from async to sync (#11168)
* convert alarmdecoder interface from async to sync

* Convert he rest of alarmdecoder rom async to sync

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py

* Update alarmdecoder.py
2017-12-17 13:07:23 +01:00
Pascal Vizeli a46ddcf6dd Add install mode to homematic (#11164) 2017-12-17 13:07:23 +01:00
Paulus Schoutsen 5ca006cc9c Don't connect to cloud if subscription expired (#11163)
* Final touch for cloud component

* Fix test
2017-12-17 13:07:23 +01:00
Adam Mills 564ed26aeb Perform logbook filtering on the worker thread (#11161) 2017-12-17 13:07:22 +01:00
Pascal Vizeli 5860267410 Resolve hostnames (#11160) 2017-12-17 13:07:22 +01:00
Pascal Vizeli ec9638f4d1 Homematic next (#11156)
* Cleanup logic & New gen of HomeMatic

* fix lint

* cleanup

* fix coverage

* cleanup

* name consistenc

* fix lint

* Rename ip

* cleanup wrong property

* fix bug

* handle callback better

* fix lint

* Running now
2017-12-17 13:07:22 +01:00
Matthew Treinish 7db8bbf385 Fix X10 commands for mochad light turn on (#11146)
* Fix X10 commands for mochad light turn on

This commit attempts to address issues that a lot of people are having
with the x10 light component. Originally this was written to use the
xdim (extended dim) X10 command. However, not every X10 dimmer device
supports the xdim command. Additionally, it turns out the number of
dim/brighness levels the X10 device supports is device specific and
there is no way to detect this (given the mostly 1 way nature of X10)

To address these issues, this commit removes the usage of xdim and
instead relies on using the 'on' command and the 'dim' command. This
should work on all x10 light devices. In an attempt to address the
different dim/brightness levels supported by different devices this
commit also adds a new optional config value, 'brightness_levels', to
specify if it's either 32, 64, or 256. By default 32 levels are used
as this is the normal case and what is documented by mochad.

Fixes #8943

* make code more readable

* fix style

* fix lint

* fix tests
2017-12-17 13:07:22 +01:00
Paulus Schoutsen f4d7bbd044 Update frontend 2017-12-15 23:36:04 -08:00
Fabian Affolter ca81180e6d Bump release to 0.60.0 2017-12-15 10:06:06 +01:00
Daniel Perna de4c8adca2 Upgrade Homematic (#11149)
* Update pyhomematic

* Update pyhomematic
2017-12-15 00:34:27 +01:00
Paulus Schoutsen 823e260c2a Disable html5 notify dependency (#11135) 2017-12-14 00:15:25 -08:00
Greg Laabs 1c8b5838cd ISY994 sensor improvements (#10805)
* Fire events for ISY994 control events

This allows hass to react directly to Insteon button presses (on switches and remotes), including presses, double-presses, and long holds

* Move change event subscription to after entity is added to hass

The event handler method requires `self.hass` to exist, which doesn't have a value until the async_added_to_hass method is called. Should eliminate a race condition.

* Overhaul binary sensors in ISY994 to be functional "out of the box"

We now smash all of the subnodes from the ISY994 in to one Hass binary_sensor, and automatically support both paradigms of state reporting that Insteon sensors can do. Sometimes a single node's state represents the sensor's state, other times two nodes are used and only "ON" events are sent from each. The logic between the two forunately do not conflict so we can support both without knowing which mode the device is in.

This also allows us to handle the heartbeat functionality that certain sensors have - we simply store the timestamp of the heartbeat as an attribute on the sensor device. It defaults to Unknown on bootup if and only if the device supports heartbeats, due to the presence of subnode 4.

* Parse the binary sensor device class from the ISY's device "type"

Now we automatically know which sensors are moisture, motion, and openings! (We also reverse the moisture sensor state, because Insteon reports ON for dry on the primary node.)

* Code review tweaks

The one material change here is that the event subscribers were moved to the `async_added_to_hass` method, as the handlers depend on things that only exist after the entity has been added.

* Handle cases where a sensor's state is unknown

When the ISY first boots up, if a battery-powered sensor has not reported in yet (due to heartbeat or a change in state), the state is unknown until it does.

* Clean up from code review

Fix coroutine await, remove unnecessary exception check, and return None when state is unknown

* Unknown value from PyISY is now -inf rather than -1

* Move heartbeat handling to a separate sensor

Now all heartbeat-compatible sensors will have a separate `binary_sensor` device that represents the battery state (on = dead)

* Add support for Unknown state, which is being added in next PyISY 

PyISY will report unknown states as the number "-inf". This is implemented in the base ISY994 component, but subcomponents that override the `state` method needed some extra logic to handle it as well.

* Change a couple try blocks to explicit None checks

* Bump PyISY to 1.1.0, now that it has been published!

* Remove -inf checking from base component

The implementation of the -inf checking was improved in another branch which has been merged in to this branch already.

* Restrict negative-node and heartbeat support to known compatible types

Not all Insteon sensors use the same subnode IDs for the same things, so we need to use different logic depending on device type. Negative node and heartbeat support is now only used for leak sensors and open/close sensors.

* Use new style string formatting

* Add binary sensor detection for pre-5.x firmware

Meant to do this originally; writing documentation revealed that this requirement was missed!
2017-12-13 20:14:56 -08:00
BryanJacobs 3473ef63af Allow using more than one keyboard remote (#11061)
* Allow using more than one keyboard remote

This sets up one thread per keyboard remote, listening for events.

* Remove enclosing block in keyboard_remote

* Remove unnecessary semantic check for keyboard_remote
2017-12-13 20:07:23 -08:00
Michael Pollett 2cced1dac3 set default utc offset to 0 (#11114) 2017-12-13 20:03:41 -08:00
Adam Mills b5d3a4736b Add problem device class (#11130) 2017-12-13 20:02:24 -08:00
Andrea Campi e627544479 Always consume the no_throttle keyword argument. (#11126)
The current code relies on the assumption that the first invocation will never specify no_throttle=True.
However that puts us in a pickle when writing unit tests: if we had a fictitious:

  def setup_platform():
    update()

  @Throttle(MIN_TIME_BETWEEN_SCANS)
  def update():
    pass

Then given multiple tests, the second and some of subsequent tests would be throttled (depending on timing).
But we also can't change that code to call `update(no_throttle=True)' because that's not currently accepted.

This diff shouldn't change the visibile behavior of any component, but allows this extra flexibility.
2017-12-13 20:01:59 -08:00
Andrea Campi d547345f90 Skip HASS emulated Hue bridges from detection. (#11128)
When refactoring the Hue support we lost a check for HASS bridges; restore that.
2017-12-13 20:00:30 -08:00
Nolan Gilley 4ec3289f9c update pyripple (#11122) 2017-12-13 21:21:14 +01:00
Philipp Schmitt 638dd25aff Add media position properties (#10076)
* Add media progress information

* Remove unnecessary comments

* Remove datetime import

* Remove pysonyavr dependency

* Fix doc syntax (D205)

* Lint fix: no-else-return

* Don't attempt to set media progress info if program is None

* Fix Python 3.4 compatibility

* Explicitely depend on pyteleloisirs

* Only update remaining play time when it changed

* Fix floot state table
2017-12-13 10:58:49 +01:00
Ryan McLean 37efd5a5cd Fixed typo in automation.py (#11116) 2017-12-13 10:17:12 +01:00
Paulus Schoutsen 168065b9bc Update Warrant (#11101)
* Update Warrant

* Lint
2017-12-12 21:12:41 -08:00
Eitan Mosenkis 95cd2035b6 Fix incorrect comment. (#11111) 2017-12-13 00:04:42 +01:00
Dan Nixon aeba81e193 Report availability for TP-Link smart bulbs (#10976) 2017-12-12 17:18:46 +01:00
Pierre Ståhl c7e327ea87 Bump pyatv to 0.3.9 (#11104) 2017-12-12 16:52:39 +01:00
Fabian Affolter ed06b8cead Use luftdaten module (#10970)
* Use luftdaten module

* Refactoring

* Check meta data

* Make name consistent

* Remove try block
2017-12-12 08:09:47 +01:00
Jan Almeroth a79c7ee217 Bump pymusiccast to version 0.1.6 (#11091) 2017-12-11 22:29:52 +01:00
Pascal Vizeli 6bf23f9167 Update tellcore-net to 0.4 (#11087)
* Update tellcore-net to 0.4

* Update requirements_all.txt
2017-12-11 18:32:48 +01:00
Fabian Affolter 1b3963299d Upgrade shodan to 1.7.7 (#11084) 2017-12-11 16:44:14 +01:00
Fabian Affolter c461a7c7e2 Upgrade youtube_dl to 2017.12.10 (#11080) 2017-12-11 13:53:01 +01:00
Fabian Affolter 0cfff13be1 Upgrade psutil to 5.4.2 (#11083) 2017-12-11 13:52:43 +01:00
Fabian Affolter 0245189670 Upgrade yarl to 0.16.0 (#11078) 2017-12-11 13:52:22 +01:00
Fabian Affolter 7777d5811f Upgrade aiohttp to 2.3.6 (#11079) 2017-12-11 13:50:55 +01:00
Pascal Vizeli 7259cc878e Allow tradfri to read the available state of the device (#11056)
* Allow tradfri to read the available state of the device

* Update tradfri.py
2017-12-11 11:34:48 +01:00
Erik Eriksson a4214afddb Volvo on call: Optional use of Scandinavian miles. Also add average fuel consumption property (#11051) 2017-12-10 13:57:44 -08:00
uchagani b078f6c342 add custom bypass status to total connect (#11042)
* add custom bypass status to total connect

* remove logger line
2017-12-10 16:02:12 -05:00
Andrea Campi 81974885ee Refactor hue to split bridge support from light platform (#10691)
* Introduce a new Hue component that knows how to talk to a Hue bridge, but doesn't actually set up lights.

* Refactor the hue lights platform to use the HueBridge class from the hue component.

* Reimplement support for multiple bridges

* Auto discover bridges.

* Provide some migration support by showing a persistent notification.

* Address most feedback from code review.

* Call load_platform from inside HueBridge.setup passing the bridge id.

Not only this looks nicer, but it also nicely solves additional bridges being added after initial setup (e.g. pairing a second bridge should work now, I believe it required a restart before).

* Add a unit test for hue_activate_scene

* Address feedback from code review.

* After feedback from @andrey-git I was able to find a way to not import phue in tests, yay!

* Inject a mock phue in a couple of places
2017-12-10 10:15:01 -08:00
Adde Lovein b2c5a9f5fe Add GPS coords to meraki (#10998) 2017-12-10 09:47:14 -08:00
maxlaverse 04cb893d10 Add a caldav calendar component (#10842)
* Add caldav component

* Code review - 1

* Code review - 2

* Sort imports
2017-12-10 17:44:28 +01:00
perfalk 3b228c78c0 Added support for cover in tellstick (#10858)
* Added support for cover in tellstick

* Fixed comments from PR

* Fixed comments from PR

* Address comments
2017-12-10 17:35:10 +01:00
tringler 4e91e6d103 This change fixes the error OSError: [WinError 193] on Windows debuggers (i.e. PyCharm) (#11034) 2017-12-09 16:58:52 -08:00
Paulus Schoutsen cb4e886a4f Make notify.html5 depend on config (#11052) 2017-12-09 13:14:16 -08:00
GreenTurtwig bee80c5b79 Add support for Logitech UE Smart Radios. (#10077)
* Add support for Logitech UE Smart Radios.

* Removed full stops to please Hound's line limit.

* Updated with requested changes.

* Fix Pylint Flake8 problem.

* Updated with requested changes.
2017-12-09 20:01:23 +01:00
Andrey Kupreychik 4479761131 Added force_update for REST sensor (#11016)
* Added force_update for REST sensor

* Linting error
2017-12-09 08:18:45 +01:00
Andrey 20f1e1609f In dev mode expose only relevant sources (#11026) 2017-12-08 17:25:16 -08:00
Matthew Treinish 1f1115f631 Serialize mochad requests (#11029)
All mochad devices are sharing a single socket interface. When multiple
threads are issuing requests to the mochad daemon at the same time the
write read cycle might get crossed between the threads. This is normally
not an issue for 1-way X10 devices because as long as the request issued
successfully and data is read over the socket then we know as much as
mochad will tell us (since there is no ACK from the request for most
X10 devices). However, where it does matter is on the device __init__()
because we're relying on the mochad daemon's internal state to take an
educated guess at the device's state to intialize things with. When
there are multiple devices being initialized at the same time the wires
can get crossed between and the wrong device state may be read.

To address this potential issue this commit adds locking using a
semaphore around all pairs of send_cmd() and read_data() (which is what
pymochad.device.Device.get_status() does internally) calls to the mochad
controller to ensure we're only ever dealing with a single request at a
time.

Fixes mtreinish/pymochad#4
2017-12-08 09:18:52 -08:00
Andrey f7c2ec19ef Change default js version to auto (#10999) 2017-12-08 09:16:26 -08:00
tschmidty69 fed7bd9473 Update snips to listen on new mqtt topic and utilize rawValue (#11020)
* Updated snips to listen on new mqtt topic and use rawValue if value not present in slot

* Too late at night

* Trying to make minor changes via web

* Update test_snips.py

* Update __init__.py

* Updated wrong branch cause I'm a monkey
2017-12-08 09:16:08 -08:00
Anders Melchiorsen 4d6070e33a Support LIFX Mini products (#10996)
* Support new LIFX products

* Remove lint
2017-12-08 08:43:37 -08:00
Anders Melchiorsen 0a7e6ac222 Ignore Sonos players with unknown hostnames (#11013) 2017-12-08 12:01:10 +01:00
Joe Lu f892c3394b Add support for Canary component and platforms (#10306)
* Add Canary component

* Made some change to how canary data is updated and stored

* Updated to use py-canary:0.1.2

* Addressed flake8 warnings

* Import canary API locally

* Import canary API locally again

* Addressed pylint errors

* Updated requirements_all.txt

* Fixed incorrect unit of measurement for air quality sensor

* Added tests for Canary component and sensors

* Updated canary component to handle exception better when initializing

* Fixed tests

* Fixed tests again

* Addressed review comments

* Fixed houndci error

* Addressed comment about camera force update

* Addressed comment regarding timeout when fetching camera image

* Updated to use py-canary==0.2.2

* Increased update frequency to 30 seconds

* Added support for Canary alarm control panel

* Address review comments

* Fixed houndci error

* Fixed lint errors

* Updated test to only test setup component / platform

* Fixed flake error

* Fixed failing test

* Uptake py-canary:0.2.3

* canary.alarm_control_panel DISARM is now mapped to canary PRIVACY mode

* Fixed failing tests

* Removed unnecessary methods

* Removed polling in canary camera component and update camera info when getting camera image

* Added more tests to cover Canary sensors

* Address review comments

* Addressed review comment in tests

* Fixed pylint errors

* Excluded canary alarm_control_panel and camera from coverage calculation
2017-12-08 10:40:45 +01:00
Marcus Schmidt 929d49ed6f Shuffle support in Sonos (#10875)
* initial commit of shuffle option for sonos

* added test

* Small adjustments to adhere to review requests

* Removed unnessesary setting of variable. Use shuffle state from soco instead
2017-12-07 14:44:06 -05:00
Lewis Juggins 3c1f8cd882 Handle OSError when forcibly turning off media_player.samsungtv (#10997) 2017-12-07 16:30:51 +00:00
Jeroen ter Heerdt f21da7cfdc Fix Egardia alarm status shown as unknown after restart (#11010) 2017-12-07 12:39:34 +01:00
Alan Fischer 39d33c97ff Added Vera scenes (#10424)
* Added Vera scenes

* Fixed flake8 issues

* Fixed comments

* Moved vera to use hass.data

* Made requested changes
2017-12-07 07:47:19 +01:00
Richard Leurs c952f2e18a Ensure Docker script files uses LF line endings to support Docker for Windows. (#10067) 2017-12-06 15:00:58 +01:00
Daniel Watkins 0fc7f37185 webostv: Ensure source exists before use (#10959)
In a case where either (a) an incorrect source name is used, or (b) the
TV isn't currently queryable (e.g. it's off), we get tracebacks because
we assume the source that we are being asked to select exists in
self._source_list.

This makes the lookup code a little more rugged, and adds in a warning
message (in place of the current exception).
2017-12-06 14:48:17 +01:00
Pascal Vizeli 9cff6c7e6a Update tradfri.py (#10991) 2017-12-06 12:44:41 +01:00
Mitko Masarliev e66268dffe Meraki AP Device tracker (#10971)
* Device tracker for meraki AP

* styles fix

* fix again

* again

* and again :)

* fix hide if away

* docs and optimization

* tests and fixes

* styles

* styles

* styles

* styles

* styles fix. Hope last

* clear track new

* changes

* fix accuracy error and requested changes

* remove meraki from .coveragerc

* tests and minor changes

* remove location
2017-12-06 09:24:20 +01:00
Paulus Schoutsen c13b510ba3 Update frontend to 20171206.0 2017-12-05 23:40:31 -08:00
Dan Nixon 5f4baa67dc Allow disabling the LEDs on TP-Link smart plugs (#10980) 2017-12-06 08:38:27 +01:00
Paulus Schoutsen 1db7e2c9d6 Merge branch 'master' into dev 2017-12-05 23:36:08 -08:00
Paulus Schoutsen fa324dce9c Merge pull request #10990 from home-assistant/release-0-59-2
0.59.2
2017-12-05 22:17:27 -08:00
Paulus Schoutsen 56c694b477 Revert pychromecast update (#10989)
* Revert pychromecast update

* Update cast.py
2017-12-05 22:11:23 -08:00
Craig J. Ward 3f764f1981 Reload closest store on api menu request (#10962)
* reload closest store on api request

* revert change from debugging
2017-12-05 22:11:22 -08:00
William Scanlon 63d6734612 Allow chime to work for wink siren/chime (#10961)
* Allow Wink siren/chimes to work

* Updated requirements_all.txt
2017-12-05 22:11:22 -08:00
Erik Eriksson f9743c29cd Upgrade tellduslive library version (closes https://github.com/home-assistant/home-assistant/issues/10922) (#10950) 2017-12-05 22:11:22 -08:00
Mateusz Drab bdb7a29586 Fix linksys_ap.py by inheriting DeviceScanner (#10947)
As per issue #8638, the class wasn't inheriting from DeviceScanner, this commit patches it up.
2017-12-05 22:11:21 -08:00
Andrey 22c36f0ad3 Require FF43 for latest js (#10941)
* Require FF43 for latest js

`Array.prototype.includes` added in Firefox 43

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes

* Update __init__.py
2017-12-05 22:11:20 -08:00
Paulus Schoutsen fd6373c7aa Version bump to 0.59.2 2017-12-05 22:10:47 -08:00
Andrey 87fe674c70 Require FF43 for latest js (#10941)
* Require FF43 for latest js

`Array.prototype.includes` added in Firefox 43

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes

* Update __init__.py
2017-12-05 22:09:41 -08:00
Paulus Schoutsen ddec566e10 Revert pychromecast update (#10989)
* Revert pychromecast update

* Update cast.py
2017-12-05 22:08:09 -08:00
William Scanlon 454d8535f8 Allow chime to work for wink siren/chime (#10961)
* Allow Wink siren/chimes to work

* Updated requirements_all.txt
2017-12-05 22:07:59 -08:00
Mitko Masarliev 8e4942088e Add option to set default hide if away for new devices (#10762)
* Option to change hide_if_away

* tests fix

* change new device defaults

* tests and requested changes

* fix assert
2017-12-05 21:56:43 -08:00
Adam Mills 3af527b1b5 Use new build path for dev translations (#10937) 2017-12-05 09:13:09 -05:00
ziotibia81 69d5738e47 Generic thermostat initial_operation_mode (#10690)
* Generic thermostat restore operation mode

* Test restore operation mode

* Fix trailing whitespace

* Fix line too long

* Fix test duplicate entity_id

* Fix test

* async_added_to_hass modify modify internal state

* Test inital_operation_mode

* More restore state tests

* Fix whitespace

* fix test_custom_setup_param

* Test "None" target temp
2017-12-05 15:00:33 +01:00
Menno Blom 379c10985b Add Ziggo Mediabox XL media_player (#10514)
* Add Ziggo Mediabox XL media_player

* Using pypi module ziggo-mediabox-xl now.

* Code review changes
2017-12-05 14:22:27 +01:00
Roman 821cf7135d Gearbest sensor (#10556)
* Added Gearbest Sensor

* Updated required files

* Fixed houndci-bout findings

* Fix tox lint errors

* Changed code according to review
Implemented library version 1.0.5

* Fixed houndci-bot findings

* Fixed tox lint issues

* Updated item schema to use has_at_least_one_key
Added conf constants

* Remove CONF_ constants and import them from homeassistant.const

* Removed CurrencyConverter from hass
Fixed couple of issues found by MartinHjelmare
2017-12-05 12:32:43 +01:00
Craig J. Ward d986bdab98 Reload closest store on api menu request (#10962)
* reload closest store on api request

* revert change from debugging
2017-12-05 10:47:48 +01:00
Stefan Lehmann 53d9fd18b7 Add ADS component (#10142)
* add ads hub, light and switch

* add binary sensor prototype

* switch: use adsvar for connection

* fix some issues with binary sensor

* fix binary sensor

* fix all platforms

* use latest pyads

* fixed error with multiple binary sensors

* add sensor

* add ads sensor

* clean up after shutdown

* ads component with platforms switch, binary_sensor, light, sensor

add locking

poll sensors at startup

update state of ads switch and light

update ads requirements

remove update() from constructors on ads platforms

omit ads coverage

ads catch read error when polling

* add ads service

* add default settings for use_notify and poll_interval

* fix too long line

* Fix style issues

* no pydocstyle errors

* Send and receive native brightness data to ADS device to prevent issues with math.floor reducing brightness -1 at every switch

* Enable non dimmable lights

* remove setting of self._state in switch

* remove polling

* Revert "remove polling"

This reverts commit 7da420f823.

* add service schema, add links to documentation

* fix naming, cleanup

* re-remove polling

* use async_added_to_hass for setup of callbacks

* fix comment.

* add callbacks for changed values

* use async_add_job for creating device notifications

* set should_poll to False for all platforms

* change should_poll to property

* add service description to services.yaml

* add for brigthness not being None

* put ads component in package

* Remove whitespace

* omit ads package
2017-12-05 09:44:22 +01:00
Mateusz Drab 38a1f06d14 Fix linksys_ap.py by inheriting DeviceScanner (#10947)
As per issue #8638, the class wasn't inheriting from DeviceScanner, this commit patches it up.
2017-12-04 18:58:52 +01:00
Paul Annekov 2e2d0f48fb don't ignore voltage data if sensor data changed (#10925) 2017-12-04 17:26:41 +01:00
Erik Eriksson 4652b8aea1 Upgrade tellduslive library version (closes https://github.com/home-assistant/home-assistant/issues/10922) (#10950) 2017-12-04 17:26:07 +01:00
dasos ef1cbd3aea Tado ignore invalid devices (#10927)
* Ignore devices without temperatures

* Typo

* Linting

* Removing return false

* Another typo. :(

* Spelling received correctly
2017-12-04 14:55:04 +01:00
drop table USERS; -- 31cedf83c7 Export climate status and target temperature to Prometheus (#10919)
* Export climate metrics to Prometheus.

This adds climate_state and temperature_c metrics for each climate
device.

* Add more climate states to state_as_number
2017-12-04 13:39:26 +01:00
Nicolas Bougues 19a97580fc Set percent unit for battery level so that history displays properly; edited variable name for consistency (#10932) 2017-12-04 08:34:42 +01:00
Dan Nixon 17f3cf0389 Report availability of TP-Link smart sockets (#10933)
* Report availability of TP-Link smart sockets

* Changes according to our style guide
2017-12-04 08:33:22 +01:00
Paulus Schoutsen 4e02300cbc Merge remote-tracking branch 'origin/master' into dev 2017-12-03 20:15:34 -08:00
Paulus Schoutsen 015cdd155c Merge pull request #10936 from home-assistant/release-0-59-1
0.59.1
2017-12-03 20:08:04 -08:00
Craig J. Ward d4e603cc6a Dominos no order fix (#10935)
* check for none

* fix error from no store being set

* typo

* Lint

* fix default as per notes. Lint fix and make closest store None not False

* update default
2017-12-03 19:36:49 -08:00
Paulus Schoutsen 7ae374e11f Update frontend to 20171204.0 (#10934) 2017-12-03 19:36:49 -08:00
Will Boyce 292b403dc3 fix ios component config generation (#10923)
Fixes: #19020
2017-12-03 19:36:49 -08:00
Daniel Perna b815898ddb Fix Notifications for Android TV (#10798)
* Fixed icon path, added dynamic icon

* Addressing requested changes

* Using DEFAULT_ICON

* Using CONF_ICON from const

* Getting hass_frontend path via import

* Lint

* Using embedded 1px transparent icon

* woof -.-

* Lint
2017-12-03 19:36:48 -08:00
Paulus Schoutsen b1855f1d1d Version bump to 0.59.1 2017-12-03 19:36:41 -08:00
Craig J. Ward bd6a17a3a5 Dominos no order fix (#10935)
* check for none

* fix error from no store being set

* typo

* Lint

* fix default as per notes. Lint fix and make closest store None not False

* update default
2017-12-03 19:34:58 -08:00
Paulus Schoutsen 29fad3fa3c Update frontend to 20171204.0 (#10934) 2017-12-03 17:59:58 -08:00
Paulus Schoutsen 0c43466225 Update coveragerc (#10931)
* Sort coveragerc

* Add climate.honeywell and vacuum.xiaomi_miio to coveragerc
2017-12-03 16:42:18 -08:00
Daniel Perna 0d6c95ac44 Fix Notifications for Android TV (#10798)
* Fixed icon path, added dynamic icon

* Addressing requested changes

* Using DEFAULT_ICON

* Using CONF_ICON from const

* Getting hass_frontend path via import

* Lint

* Using embedded 1px transparent icon

* woof -.-

* Lint
2017-12-03 15:08:10 -08:00
Will Boyce 6776e942d7 fix ios component config generation (#10923)
Fixes: #19020
2017-12-03 14:59:22 -08:00
Brent Hughes 879e32f670 Add Min and Event Count Metrics To Prometheus (#10530)
* Added min and Events sensor types to prometheus

* Updated prometheus client and fixed invalid swith state

* Added metric to count number of times an automation is triggered

* Removed assumption that may not apply to everybody

* Fixed tests

* Updated requirements_test_all

* Fixed unit tests
2017-12-03 23:39:54 +01:00
Oliver 3a246df544 Don't repeat getting receiver name on each update / pushed to denonavr 0.5.5 (#10915) 2017-12-03 21:51:32 +01:00
Fabian Affolter 9577525b0b Add Alpha Vantage sensor (#10873)
* Add Alpha Vantage sensor

* Remove data object

* Remove unused vars and change return

* Fix typo
2017-12-03 21:34:59 +01:00
Touliloup 6b410d8076 Correction of Samsung Power OFF behaviour (#10907)
* Correction of Samsung Power OFF behaviour

Addition of a delay after powering OFF a Samsung TV, this avoid status
update from powering the TV back ON.
Deletion of update() return statement, return value not used.

* Rename self._end_of_power_off_command into self._end_of_power_off

* Removal of unused line break in Samsung TV component
2017-12-03 18:34:45 +01:00
Ludovico de Nittis 9e82433a3e Add iAlarm support (#10878)
* Add iAlarm support

* Minor fixes to iAlarm

* Rename ialarmpanel to ialarm and add a check for the host value

* corrections in the value validation of ialarm

* add a missing period on ialarm
2017-12-03 16:48:12 +01:00
Erik Eriksson 8ceaa72ba3 Update eliqonline.py (#10914)
Channel id is now required (change in API)
2017-12-03 16:48:07 +01:00
Fabian Affolter fce994ea76 Bump dev to 0.60.0.dev0 (#10912) 2017-12-03 16:47:21 +01:00
Nicko van Someren 4390fed168 Unpacking RESTful sensor JSON results into attributes. (#10753)
* Added support for extracting JSON attributes from RESTful values

Setting the json_attributes configuration option to true on the
RESTful sensor will cause the result of the REST request to be parsed
as a JSON string and if successful the resulting dictionary will be
used for the attributes of the sensor.

* Added support for extracting JSON attributes from RESTful values

Setting the json_attributes configuration option to true on the
RESTful sensor will cause the result of the REST request to be parsed
as a JSON string and if successful the resulting dictionary will be
used for the attributes of the sensor.

* Added requirement that RESTful JSON results used as attributes must be
objects, not lists.

* Expanded test coverage to test REFTful JSON attributes with and
without a value template.

* Added support for extracting JSON attributes from RESTful values

Setting the json_attributes configuration option to true on the
RESTful sensor will cause the result of the REST request to be parsed
as a JSON string and if successful the resulting dictionary will be
used for the attributes of the sensor.

* Added requirement that RESTful JSON results used as attributes must be
objects, not lists.

* Expanded test coverage to test REFTful JSON attributes with and
without a value template.

* sensor.envirophat: add missing requirement (#7451)

Adding requirements that is not explicitly pulled in by the library
that manages the Enviro pHAT.

* PyPI Openzwave (#7415)

* Remove default zwave config path

PYOZW now has much more comprehensive default handling for the config
path (in src-lib/libopenzwave/libopenzwave.pyx:getConfig()). It looks in
the same place we were looking, plus _many_ more. It will certainly do a
much better job of finding the config files than we will (and will be
updated as the library is changed, so we don't end up chasing it). The
getConfig() method has been there for a while, but was subsntially
improved recently.

This change simply leaves the config_path as None if it is not
specified, which will trigger the default handling in PYOZW.

* Install python-openzwave from PyPI

As of version 0.4, python-openzwave supports installation from PyPI,
which means we can use our 'normal' dependency management tooling to
install it. Yay.

This uses the default 'embed' build (which goes and downloads
statically sources to avoid having to compile anything locally). Check
out the python-openzwave readme for more details.

* Add python-openzwave deps to .travis.yml

Python OpenZwave require the libudev headers to build. This adds the
libudev-dev package to Travis runs via the 'apt' addon for Travis.

Thanks to @MartinHjelmare for this fix.

* Update docker build for PyPI openzwave

Now that PYOZW can be install from PyPI, the docker image build process
can be simplified to remove the explicit compilation of PYOZW.

* Add datadog component (#7158)

* Add datadog component

* Improve test_invalid_config datadog test

* Use assert_setup_component for test setup

* Fix object type for default KNX port

#7429 describes a TypeError that is raised if the port is omitted in the config for the KNX component (integer is required (got type str)). This commit changes the default port from a string to an integer. I expect this will resolve that issue...

* Added support for extracting JSON attributes from RESTful values

Setting the json_attributes configuration option to true on the
RESTful sensor will cause the result of the REST request to be parsed
as a JSON string and if successful the resulting dictionary will be
used for the attributes of the sensor.

* Added requirement that RESTful JSON results used as attributes must be
objects, not lists.

* Expanded test coverage to test REFTful JSON attributes with and
without a value template.

* Added support for extracting JSON attributes from RESTful values

Setting the json_attributes configuration option to true on the
RESTful sensor will cause the result of the REST request to be parsed
as a JSON string and if successful the resulting dictionary will be
used for the attributes of the sensor.

* Added requirement that RESTful JSON results used as attributes must be
objects, not lists.

* Expanded test coverage to test REFTful JSON attributes with and
without a value template.

* Added support for extracting JSON attributes from RESTful values

Setting the json_attributes configuration option to true on the
RESTful sensor will cause the result of the REST request to be parsed
as a JSON string and if successful the resulting dictionary will be
used for the attributes of the sensor.

* Added requirement that RESTful JSON results used as attributes must be
objects, not lists.

* Expanded test coverage to test REFTful JSON attributes with and
without a value template.

* Fixed breaks cause by manual upstream merge.

* Added one extra blank line to make PyLint happy.

* Switched json_attributes to be a list of keys rather than a boolean.

The value of json_attributes can now be either a comma sepaated list
of key names or a YAML list of key names. Only matching keys in a
retuned JSON dictionary will be mapped to sensor attributes.

Updated test cases to handle json_attributes being a list.

Also fixed two minor issues arrising from manual merge with 0.58 master.

* Added an explicit default value to the json_attributes config entry.

* Removed self.update() from __init__() body.

* Expended unit tests for error cases of json_attributes processing.

* Align quotes
2017-12-03 16:30:25 +01:00
Paolo Bonzini 0f8e48c26d More declarative timeout syntax for manual alarm control panel. (#10738)
More declarative timeout syntax for manual alarm control panel
2017-12-03 13:52:31 +01:00
Fabian Affolter 2d556486bf Merge branch 'master' into dev 2017-12-03 13:28:25 +01:00
John Arild Berentsen 29f47d58bc Bugfix #10902 (#10904) 2017-12-03 00:15:57 +01:00
PhracturedBlue 8947052405 Fix issues from review of ecobee weather component (#10903)
* Fix issues from review

* Don't use STATE_UNKNOWN
2017-12-02 22:44:55 +01:00
raymccarthy 475b7896e2 Pybotvac multi (#10843)
* Update requirements_all.txt

* Update neato.py
2017-12-02 15:44:24 +01:00
PhracturedBlue b2a2cb3fd8 Update ecobee version to fix stack-trace issue (#10894) 2017-12-02 07:56:35 +02:00
133 changed files with 7285 additions and 1055 deletions
+23 -10
View File
@@ -11,6 +11,9 @@ omit =
homeassistant/components/abode.py
homeassistant/components/*/abode.py
homeassistant/components/ads/__init__.py
homeassistant/components/*/ads.py
homeassistant/components/alarmdecoder.py
homeassistant/components/*/alarmdecoder.py
@@ -85,7 +88,7 @@ omit =
homeassistant/components/hive.py
homeassistant/components/*/hive.py
homeassistant/components/homematic.py
homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py
homeassistant/components/insteon_local.py
@@ -261,8 +264,10 @@ omit =
homeassistant/components/*/zoneminder.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/egardia.py
homeassistant/components/alarm_control_panel/ialarm.py
homeassistant/components/alarm_control_panel/manual_mqtt.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
@@ -279,14 +284,16 @@ omit =
homeassistant/components/binary_sensor/rest.py
homeassistant/components/binary_sensor/tapsaff.py
homeassistant/components/browser.py
homeassistant/components/calendar/caldav.py
homeassistant/components/calendar/todoist.py
homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/canary.py
homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/onvif.py
homeassistant/components/camera/ring.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py
homeassistant/components/camera/yi.py
homeassistant/components/climate/ephember.py
@@ -294,6 +301,7 @@ omit =
homeassistant/components/climate/flexit.py
homeassistant/components/climate/heatmiser.py
homeassistant/components/climate/homematic.py
homeassistant/components/climate/honeywell.py
homeassistant/components/climate/knx.py
homeassistant/components/climate/oem.py
homeassistant/components/climate/proliphix.py
@@ -331,10 +339,10 @@ omit =
homeassistant/components/device_tracker/sky_hub.py
homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tado.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tile.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/trackr.py
homeassistant/components/device_tracker/ubus.py
@@ -352,8 +360,8 @@ omit =
homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py
homeassistant/components/light/avion.py
homeassistant/components/light/blinkt.py
homeassistant/components/light/blinksticklight.py
homeassistant/components/light/blinkt.py
homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py
homeassistant/components/light/flux_led.py
@@ -364,8 +372,8 @@ omit =
homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py
homeassistant/components/light/osramlightify.py
homeassistant/components/light/rpi_gpio_pwm.py
homeassistant/components/light/piglow.py
homeassistant/components/light/rpi_gpio_pwm.py
homeassistant/components/light/sensehat.py
homeassistant/components/light/tikteck.py
homeassistant/components/light/tplink.py
@@ -376,9 +384,9 @@ omit =
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py
homeassistant/components/lirc.py
homeassistant/components/lock/lockitron.py
homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py
homeassistant/components/lock/lockitron.py
homeassistant/components/lock/sesame.py
homeassistant/components/media_extractor.py
homeassistant/components/media_player/anthemav.py
@@ -420,11 +428,13 @@ omit =
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/spotify.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/ue_smart_radio.py
homeassistant/components/media_player/vizio.py
homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/yamaha_musiccast.py
homeassistant/components/media_player/ziggo_mediabox_xl.py
homeassistant/components/mycroft.py
homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py
@@ -472,6 +482,7 @@ omit =
homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/airvisual.py
homeassistant/components/sensor/alpha_vantage.py
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py
@@ -482,8 +493,8 @@ omit =
homeassistant/components/sensor/bom.py
homeassistant/components/sensor/broadlink.py
homeassistant/components/sensor/buienradar.py
homeassistant/components/sensor/citybikes.py
homeassistant/components/sensor/cert_expiry.py
homeassistant/components/sensor/citybikes.py
homeassistant/components/sensor/comed_hourly_pricing.py
homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/crimereports.py
@@ -511,6 +522,7 @@ omit =
homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/gearbest.py
homeassistant/components/sensor/geizhals.py
homeassistant/components/sensor/gitter.py
homeassistant/components/sensor/glances.py
@@ -621,8 +633,8 @@ omit =
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/snmp.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/xiaomi_miio.py
homeassistant/components/telegram_bot/*
@@ -631,7 +643,9 @@ omit =
homeassistant/components/tts/baidu.py
homeassistant/components/tts/microsoft.py
homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py
homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/metoffice.py
@@ -640,7 +654,6 @@ omit =
homeassistant/components/weather/zamg.py
homeassistant/components/zeroconf.py
homeassistant/components/zwave/util.py
homeassistant/components/vacuum/mqtt.py
[report]
# Regexes for lines to exclude from consideration
+3
View File
@@ -0,0 +1,3 @@
# Ensure Docker script files uses LF to support Docker for Windows.
setup_docker_prereqs eol=lf
/virtualization/Docker/scripts/* eol=lf
+1
View File
@@ -54,6 +54,7 @@ homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen
homeassistant/components/sensor/sytadin.py @gautric
+1 -1
View File
@@ -230,7 +230,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
if os.path.basename(sys.argv[0]) == '__main__.py':
modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if
+217
View File
@@ -0,0 +1,217 @@
"""
ADS Component.
For more details about this component, please refer to the documentation.
https://home-assistant.io/components/ads/
"""
import os
import threading
import struct
import logging
import ctypes
from collections import namedtuple
import voluptuous as vol
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
EVENT_HOMEASSISTANT_STOP
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyads==2.2.6']
_LOGGER = logging.getLogger(__name__)
DATA_ADS = 'data_ads'
# Supported Types
ADSTYPE_INT = 'int'
ADSTYPE_UINT = 'uint'
ADSTYPE_BYTE = 'byte'
ADSTYPE_BOOL = 'bool'
DOMAIN = 'ads'
# config variable names
CONF_ADS_VAR = 'adsvar'
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
CONF_ADS_TYPE = 'adstype'
CONF_ADS_FACTOR = 'factor'
CONF_ADS_VALUE = 'value'
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT,
ADSTYPE_BYTE]),
vol.Required(CONF_ADS_VALUE): cv.match_all
})
def setup(hass, config):
"""Set up the ADS component."""
import pyads
conf = config[DOMAIN]
# get ads connection parameters from config
net_id = conf.get(CONF_DEVICE)
ip_address = conf.get(CONF_IP_ADDRESS)
port = conf.get(CONF_PORT)
# create a new ads connection
client = pyads.Connection(net_id, port, ip_address)
# add some constants to AdsHub
AdsHub.ADS_TYPEMAP = {
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
ADSTYPE_INT: pyads.PLCTYPE_INT,
ADSTYPE_UINT: pyads.PLCTYPE_UINT,
}
AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL
AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE
AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT
AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT
AdsHub.ADSError = pyads.ADSError
# connect to ads client and try to connect
try:
ads = AdsHub(client)
except pyads.pyads.ADSError:
_LOGGER.error(
'Could not connect to ADS host (netid=%s, port=%s)', net_id, port
)
return False
# add ads hub to hass data collection, listen to shutdown
hass.data[DATA_ADS] = ads
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown)
def handle_write_data_by_name(call):
"""Write a value to the connected ADS device."""
ads_var = call.data.get(CONF_ADS_VAR)
ads_type = call.data.get(CONF_ADS_TYPE)
value = call.data.get(CONF_ADS_VALUE)
try:
ads.write_by_name(ads_var, value, ads.ADS_TYPEMAP[ads_type])
except pyads.ADSError as err:
_LOGGER.error(err)
# load descriptions from services.yaml
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
descriptions[SERVICE_WRITE_DATA_BY_NAME],
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME
)
return True
# tuple to hold data needed for notification
NotificationItem = namedtuple(
'NotificationItem', 'hnotify huser name plc_datatype callback'
)
class AdsHub:
"""Representation of a PyADS connection."""
def __init__(self, ads_client):
"""Initialize the ADS Hub."""
self._client = ads_client
self._client.open()
# all ADS devices are registered here
self._devices = []
self._notification_items = {}
self._lock = threading.Lock()
def shutdown(self, *args, **kwargs):
"""Shutdown ADS connection."""
_LOGGER.debug('Shutting down ADS')
for notification_item in self._notification_items.values():
self._client.del_device_notification(
notification_item.hnotify,
notification_item.huser
)
_LOGGER.debug(
'Deleting device notification %d, %d',
notification_item.hnotify, notification_item.huser
)
self._client.close()
def register_device(self, device):
"""Register a new device."""
self._devices.append(device)
def write_by_name(self, name, value, plc_datatype):
"""Write a value to the device."""
with self._lock:
return self._client.write_by_name(name, value, plc_datatype)
def read_by_name(self, name, plc_datatype):
"""Read a value from the device."""
with self._lock:
return self._client.read_by_name(name, plc_datatype)
def add_device_notification(self, name, plc_datatype, callback):
"""Add a notification to the ADS devices."""
from pyads import NotificationAttrib
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
with self._lock:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback
)
hnotify = int(hnotify)
_LOGGER.debug(
'Added Device Notification %d for variable %s', hnotify, name
)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback
)
def _device_notification_callback(self, addr, notification, huser):
"""Handle device notifications."""
contents = notification.contents
hnotify = int(contents.hNotification)
_LOGGER.debug('Received Notification %d', hnotify)
data = contents.data
try:
notification_item = self._notification_items[hnotify]
except KeyError:
_LOGGER.debug('Unknown Device Notification handle: %d', hnotify)
return
# parse data to desired datatype
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
elif notification_item.plc_datatype == self.PLCTYPE_INT:
value = struct.unpack('<h', bytearray(data)[:2])[0]
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
value = struct.unpack('<B', bytearray(data)[:1])[0]
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
value = struct.unpack('<H', bytearray(data)[:2])[0]
else:
value = bytearray(data)
_LOGGER.warning('No callback available for this datatype.')
# execute callback
notification_item.callback(notification_item.name, value)
@@ -0,0 +1,15 @@
# Describes the format for available ADS services
write_data_by_name:
description: Write a value to the connected ADS device.
fields:
adsvar:
description: The name of the variable to write to.
example: '.global_var'
adstype:
description: The data type of the variable to write to.
example: 'int'
value:
description: The value to write to the variable.
example: 1
@@ -7,30 +7,21 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
import asyncio
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarmdecoder import (DATA_AD,
SIGNAL_PANEL_MESSAGE)
from homeassistant.components.alarmdecoder import (
DATA_AD, SIGNAL_PANEL_MESSAGE)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up for AlarmDecoder alarm panels."""
_LOGGER.debug("AlarmDecoderAlarmPanel: setup")
device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
async_add_devices([device])
add_devices([AlarmDecoderAlarmPanel()])
return True
@@ -38,38 +29,35 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an AlarmDecoder-based alarm panel."""
def __init__(self, name, hass):
def __init__(self):
"""Initialize the alarm panel."""
self._display = ""
self._name = name
self._state = STATE_UNKNOWN
_LOGGER.debug("Setting up panel")
self._name = "Alarm Panel"
self._state = None
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_PANEL_MESSAGE, self._message_callback)
@callback
def _message_callback(self, message):
if message.alarm_sounding or message.fire_alarm:
if self._state != STATE_ALARM_TRIGGERED:
self._state = STATE_ALARM_TRIGGERED
self.async_schedule_update_ha_state()
self.schedule_update_ha_state()
elif message.armed_away:
if self._state != STATE_ALARM_ARMED_AWAY:
self._state = STATE_ALARM_ARMED_AWAY
self.async_schedule_update_ha_state()
self.schedule_update_ha_state()
elif message.armed_home:
if self._state != STATE_ALARM_ARMED_HOME:
self._state = STATE_ALARM_ARMED_HOME
self.async_schedule_update_ha_state()
self.schedule_update_ha_state()
else:
if self._state != STATE_ALARM_DISARMED:
self._state = STATE_ALARM_DISARMED
self.async_schedule_update_ha_state()
self.schedule_update_ha_state()
@property
def name(self):
@@ -91,26 +79,20 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Return the state of the device."""
return self._state
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
def alarm_disarm(self, code=None):
"""Send disarm command."""
_LOGGER.debug("alarm_disarm: %s", code)
if code:
_LOGGER.debug("alarm_disarm: sending %s1", str(code))
self.hass.data[DATA_AD].send("{!s}1".format(code))
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
def alarm_arm_away(self, code=None):
"""Send arm away command."""
_LOGGER.debug("alarm_arm_away: %s", code)
if code:
_LOGGER.debug("alarm_arm_away: sending %s2", str(code))
self.hass.data[DATA_AD].send("{!s}2".format(code))
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
def alarm_arm_home(self, code=None):
"""Send arm home command."""
_LOGGER.debug("alarm_arm_home: %s", code)
if code:
_LOGGER.debug("alarm_arm_home: sending %s3", str(code))
self.hass.data[DATA_AD].send("{!s}3".format(code))
@@ -0,0 +1,92 @@
"""
Support for Canary alarm.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.canary/
"""
import logging
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.canary import DATA_CANARY
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
DEPENDENCIES = ['canary']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Canary alarms."""
data = hass.data[DATA_CANARY]
devices = []
for location in data.locations:
devices.append(CanaryAlarm(data, location.location_id))
add_devices(devices, True)
class CanaryAlarm(AlarmControlPanel):
"""Representation of a Canary alarm control panel."""
def __init__(self, data, location_id):
"""Initialize a Canary security camera."""
self._data = data
self._location_id = location_id
@property
def name(self):
"""Return the name of the alarm."""
location = self._data.get_location(self._location_id)
return location.name
@property
def state(self):
"""Return the state of the device."""
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
LOCATION_MODE_NIGHT
location = self._data.get_location(self._location_id)
if location.is_private:
return STATE_ALARM_DISARMED
mode = location.mode
if mode.name == LOCATION_MODE_AWAY:
return STATE_ALARM_ARMED_AWAY
elif mode.name == LOCATION_MODE_HOME:
return STATE_ALARM_ARMED_HOME
elif mode.name == LOCATION_MODE_NIGHT:
return STATE_ALARM_ARMED_NIGHT
else:
return None
@property
def device_state_attributes(self):
"""Return the state attributes."""
location = self._data.get_location(self._location_id)
return {
'private': location.is_private
}
def alarm_disarm(self, code=None):
"""Send disarm command."""
location = self._data.get_location(self._location_id)
self._data.set_location_mode(self._location_id, location.mode.name,
True)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
from canary.api import LOCATION_MODE_HOME
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
from canary.api import LOCATION_MODE_AWAY
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
from canary.api import LOCATION_MODE_NIGHT
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
@@ -4,30 +4,45 @@ Demo platform that has two fake alarm control panels.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import datetime
import homeassistant.components.alarm_control_panel.manual as manual
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED, CONF_PENDING_TIME)
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo alarm control panel platform."""
add_devices([
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False, {
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
STATE_ALARM_ARMED_AWAY: {
CONF_PENDING_TIME: 5
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_HOME: {
CONF_PENDING_TIME: 5
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_NIGHT: {
CONF_PENDING_TIME: 5
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_DISARMED: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
CONF_PENDING_TIME: 5
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_TRIGGERED: {
CONF_PENDING_TIME: 5
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
},
}),
])
@@ -116,12 +116,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
"""Return the state of the device."""
return self._status
@property
def should_poll(self):
"""Poll if no report server is enabled."""
if not self._rs_enabled:
return True
return False
def handle_system_status_event(self, event):
"""Handle egardia_system_status_event."""
if event.data.get('status') is not None:
statuscode = event.data.get('status')
status = self.lookupstatusfromcode(statuscode)
self.parsestatus(status)
self.schedule_update_ha_state()
def listen_to_system_status(self):
"""Subscribe to egardia_system_status event."""
@@ -161,9 +169,8 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def update(self):
"""Update the alarm status."""
if not self._rs_enabled:
status = self._egardiasystem.getstate()
self.parsestatus(status)
status = self._egardiasystem.getstate()
self.parsestatus(status)
def alarm_disarm(self, code=None):
"""Send disarm command."""
@@ -0,0 +1,107 @@
"""
Interfaces with iAlarm control panels.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.ialarm/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, CONF_NAME)
REQUIREMENTS = ['pyialarm==0.2']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'iAlarm'
def no_application_protocol(value):
"""Validate that value is without the application protocol."""
protocol_separator = "://"
if not value or protocol_separator in value:
raise vol.Invalid(
'Invalid host, {} is not allowed'.format(protocol_separator))
return value
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an iAlarm control panel."""
name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
url = 'http://{}'.format(host)
ialarm = IAlarmPanel(name, username, password, url)
add_devices([ialarm], True)
class IAlarmPanel(alarm.AlarmControlPanel):
"""Represent an iAlarm status."""
def __init__(self, name, username, password, url):
"""Initialize the iAlarm status."""
from pyialarm import IAlarm
self._name = name
self._username = username
self._password = password
self._url = url
self._state = None
self._client = IAlarm(username, password, url)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Return the state of the device."""
status = self._client.get_status()
_LOGGER.debug('iAlarm status: %s', status)
if status:
status = int(status)
if status == self._client.DISARMED:
state = STATE_ALARM_DISARMED
elif status == self._client.ARMED_AWAY:
state = STATE_ALARM_ARMED_AWAY
elif status == self._client.ARMED_STAY:
state = STATE_ALARM_ARMED_HOME
else:
state = None
self._state = state
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._client.disarm()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._client.arm_away()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._client.arm_stay()
@@ -16,24 +16,40 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_DISARMED, STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, CONF_CODE,
CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
CONF_DELAY_TIME, 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
CONF_CODE_TEMPLATE = 'code_template'
DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_PENDING_TIME = 60
DEFAULT_TRIGGER_TIME = 120
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED,
STATE_ALARM_ARMED_CUSTOM_BYPASS]
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_TRIGGERED]
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_DISARMED]
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
ATTR_POST_PENDING_STATE = 'post_pending_state'
def _state_validator(config):
config = copy.deepcopy(config)
for state in SUPPORTED_PRETRIGGER_STATES:
if CONF_DELAY_TIME not in config[state]:
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
if CONF_TRIGGER_TIME not in config[state]:
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
for state in SUPPORTED_PENDING_STATES:
if CONF_PENDING_TIME not in config[state]:
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
@@ -41,28 +57,44 @@ def _state_validator(config):
return config
STATE_SETTING_SCHEMA = vol.Schema({
vol.Optional(CONF_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=0))
})
def _state_schema(state):
schema = {}
if state in SUPPORTED_PRETRIGGER_STATES:
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
return vol.Schema(schema)
PLATFORM_SCHEMA = vol.Schema(vol.All({
vol.Required(CONF_PLATFORM): 'manual',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS,
default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
_state_schema(STATE_ALARM_ARMED_AWAY),
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
_state_schema(STATE_ALARM_ARMED_HOME),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
_state_schema(STATE_ALARM_ARMED_NIGHT),
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
vol.Optional(STATE_ALARM_DISARMED, default={}):
_state_schema(STATE_ALARM_DISARMED),
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
_state_schema(STATE_ALARM_TRIGGERED),
}, _state_validator))
_LOGGER = logging.getLogger(__name__)
@@ -74,8 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hass,
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_CODE_TEMPLATE),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config
)])
@@ -86,27 +117,37 @@ class ManualAlarm(alarm.AlarmControlPanel):
Representation of an alarm status.
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 the previous state
or disarm if `disarm_after_trigger` is true.
When triggered, will be pending for the triggering state's 'delay_time'
plus the triggered state's 'pending_time'.
After that will be triggered for 'trigger_time', after that we return to
the previous state or disarm if `disarm_after_trigger` is true.
A trigger_time of zero disables the alarm_trigger service.
"""
def __init__(self, hass, name, code, pending_time, trigger_time,
def __init__(self, hass, name, code, code_template,
disarm_after_trigger, config):
"""Init the manual alarm panel."""
self._state = STATE_ALARM_DISARMED
self._hass = hass
self._name = name
self._code = str(code) if code else None
self._trigger_time = datetime.timedelta(seconds=trigger_time)
if code_template:
self._code = code_template
self._code.hass = hass
else:
self._code = code or None
self._disarm_after_trigger = disarm_after_trigger
self._pre_trigger_state = self._state
self._previous_state = self._state
self._state_ts = None
self._pending_time_by_state = {}
for state in SUPPORTED_PENDING_STATES:
self._pending_time_by_state[state] = datetime.timedelta(
seconds=config[state][CONF_PENDING_TIME])
self._delay_time_by_state = {
state: config[state][CONF_DELAY_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES}
@property
def should_poll(self):
@@ -121,15 +162,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
@property
def state(self):
"""Return the state of the device."""
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
if self._state == STATE_ALARM_TRIGGERED:
if self._within_pending_time(self._state):
return STATE_ALARM_PENDING
elif (self._state_ts + self._pending_time_by_state[self._state] +
self._trigger_time) < dt_util.utcnow():
trigger_time = self._trigger_time_by_state[self._previous_state]
if (self._state_ts + self._pending_time(self._state) +
trigger_time) < dt_util.utcnow():
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
else:
self._state = self._pre_trigger_state
self._state = self._previous_state
return self._state
if self._state in SUPPORTED_PENDING_STATES and \
@@ -138,9 +180,21 @@ class ManualAlarm(alarm.AlarmControlPanel):
return self._state
def _within_pending_time(self, state):
@property
def _active_state(self):
if self.state == STATE_ALARM_PENDING:
return self._previous_state
else:
return self._state
def _pending_time(self, state):
pending_time = self._pending_time_by_state[state]
return self._state_ts + pending_time > dt_util.utcnow()
if state == STATE_ALARM_TRIGGERED:
pending_time += self._delay_time_by_state[self._previous_state]
return pending_time
def _within_pending_time(self, state):
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
@property
def code_format(self):
@@ -185,26 +239,35 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
def alarm_trigger(self, code=None):
"""Send alarm trigger command. No code needed."""
self._pre_trigger_state = self._state
"""
Send alarm trigger command.
No code needed, a trigger time of zero for the current state
disables the alarm.
"""
if not self._trigger_time_by_state[self._active_state]:
return
self._update_state(STATE_ALARM_TRIGGERED)
def _update_state(self, state):
if self._state == state:
return
self._previous_state = self._state
self._state = state
self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state()
pending_time = self._pending_time_by_state[state]
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + pending_time)
trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + self._trigger_time + pending_time)
self._state_ts + pending_time + trigger_time)
elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time(
self._hass, self.async_update_ha_state,
@@ -212,7 +275,14 @@ class ManualAlarm(alarm.AlarmControlPanel):
def _validate_code(self, code, state):
"""Validate given code."""
check = self._code is None or code == self._code
if self._code is None:
return True
if isinstance(self._code, str):
alarm_code = self._code
else:
alarm_code = self._code.render(from_state=self._state,
to_state=state)
check = not alarm_code or code == alarm_code
if not check:
_LOGGER.warning("Invalid code given for %s", state)
return check
@@ -223,6 +293,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
state_attr = {}
if self.state == STATE_ALARM_PENDING:
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
state_attr[ATTR_POST_PENDING_STATE] = self._state
return state_attr
@@ -16,8 +16,8 @@ import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
CONF_DISARM_AFTER_TRIGGER)
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
import homeassistant.components.mqtt as mqtt
from homeassistant.helpers.event import async_track_state_change
@@ -26,28 +26,44 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time
CONF_CODE_TEMPLATE = 'code_template'
CONF_PAYLOAD_DISARM = 'payload_disarm'
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_PENDING_TIME = 60
DEFAULT_TRIGGER_TIME = 120
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False
DEFAULT_ARM_AWAY = 'ARM_AWAY'
DEFAULT_ARM_HOME = 'ARM_HOME'
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
DEFAULT_DISARM = 'DISARM'
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED]
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED]
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_TRIGGERED]
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
if state != STATE_ALARM_DISARMED]
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
ATTR_POST_PENDING_STATE = 'post_pending_state'
def _state_validator(config):
config = copy.deepcopy(config)
for state in SUPPORTED_PRETRIGGER_STATES:
if CONF_DELAY_TIME not in config[state]:
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
if CONF_TRIGGER_TIME not in config[state]:
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
for state in SUPPORTED_PENDING_STATES:
if CONF_PENDING_TIME not in config[state]:
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
@@ -55,27 +71,44 @@ def _state_validator(config):
return config
STATE_SETTING_SCHEMA = vol.Schema({
vol.Optional(CONF_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=0))
})
def _state_schema(state):
schema = {}
if state in SUPPORTED_PRETRIGGER_STATES:
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta)
return vol.Schema(schema)
DEPENDENCIES = ['mqtt']
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): 'manual_mqtt',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
_state_schema(STATE_ALARM_ARMED_AWAY),
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
_state_schema(STATE_ALARM_ARMED_HOME),
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
_state_schema(STATE_ALARM_ARMED_NIGHT),
vol.Optional(STATE_ALARM_DISARMED, default={}):
_state_schema(STATE_ALARM_DISARMED),
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
_state_schema(STATE_ALARM_TRIGGERED),
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
@@ -93,8 +126,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hass,
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_CODE_TEMPLATE),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
config.get(mqtt.CONF_STATE_TOPIC),
config.get(mqtt.CONF_COMMAND_TOPIC),
@@ -111,13 +143,15 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
Representation of an alarm status.
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 the previous state
or disarm if `disarm_after_trigger` is true.
When triggered, will be pending for the triggering state's 'delay_time'
plus the triggered state's 'pending_time'.
After that will be triggered for 'trigger_time', after that we return to
the previous state or disarm if `disarm_after_trigger` is true.
A trigger_time of zero disables the alarm_trigger service.
"""
def __init__(self, hass, name, code, pending_time,
trigger_time, disarm_after_trigger,
def __init__(self, hass, name, code, code_template,
disarm_after_trigger,
state_topic, command_topic, qos,
payload_disarm, payload_arm_home, payload_arm_away,
payload_arm_night, config):
@@ -125,17 +159,24 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_DISARMED
self._hass = hass
self._name = name
self._code = str(code) if code else None
self._pending_time = datetime.timedelta(seconds=pending_time)
self._trigger_time = datetime.timedelta(seconds=trigger_time)
if code_template:
self._code = code_template
self._code.hass = hass
else:
self._code = code or None
self._disarm_after_trigger = disarm_after_trigger
self._pre_trigger_state = self._state
self._previous_state = self._state
self._state_ts = None
self._pending_time_by_state = {}
for state in SUPPORTED_PENDING_STATES:
self._pending_time_by_state[state] = datetime.timedelta(
seconds=config[state][CONF_PENDING_TIME])
self._delay_time_by_state = {
state: config[state][CONF_DELAY_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
self._trigger_time_by_state = {
state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES}
self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES}
self._state_topic = state_topic
self._command_topic = command_topic
@@ -158,15 +199,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
@property
def state(self):
"""Return the state of the device."""
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
if self._state == STATE_ALARM_TRIGGERED:
if self._within_pending_time(self._state):
return STATE_ALARM_PENDING
elif (self._state_ts + self._pending_time_by_state[self._state] +
self._trigger_time) < dt_util.utcnow():
trigger_time = self._trigger_time_by_state[self._previous_state]
if (self._state_ts + self._pending_time(self._state) +
trigger_time) < dt_util.utcnow():
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
else:
self._state = self._pre_trigger_state
self._state = self._previous_state
return self._state
if self._state in SUPPORTED_PENDING_STATES and \
@@ -175,9 +217,21 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
return self._state
def _within_pending_time(self, state):
@property
def _active_state(self):
if self.state == STATE_ALARM_PENDING:
return self._previous_state
else:
return self._state
def _pending_time(self, state):
pending_time = self._pending_time_by_state[state]
return self._state_ts + pending_time > dt_util.utcnow()
if state == STATE_ALARM_TRIGGERED:
pending_time += self._delay_time_by_state[self._previous_state]
return pending_time
def _within_pending_time(self, state):
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
@property
def code_format(self):
@@ -215,26 +269,35 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
self._update_state(STATE_ALARM_ARMED_NIGHT)
def alarm_trigger(self, code=None):
"""Send alarm trigger command. No code needed."""
self._pre_trigger_state = self._state
"""
Send alarm trigger command.
No code needed, a trigger time of zero for the current state
disables the alarm.
"""
if not self._trigger_time_by_state[self._active_state]:
return
self._update_state(STATE_ALARM_TRIGGERED)
def _update_state(self, state):
if self._state == state:
return
self._previous_state = self._state
self._state = state
self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state()
pending_time = self._pending_time_by_state[state]
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED:
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + pending_time)
trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time(
self._hass, self.async_update_ha_state,
self._state_ts + self._trigger_time + pending_time)
self._state_ts + pending_time + trigger_time)
elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time(
self._hass, self.async_update_ha_state,
@@ -242,7 +305,14 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
def _validate_code(self, code, state):
"""Validate given code."""
check = self._code is None or code == self._code
if self._code is None:
return True
if isinstance(self._code, str):
alarm_code = self._code
else:
alarm_code = self._code.render(from_state=self._state,
to_state=state)
check = not alarm_code or code == alarm_code
if not check:
_LOGGER.warning("Invalid code given for %s", state)
return check
@@ -253,6 +323,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
state_attr = {}
if self.state == STATE_ALARM_PENDING:
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
state_attr[ATTR_POST_PENDING_STATE] = self._state
return state_attr
@@ -14,7 +14,9 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
STATE_ALARM_ARMED_CUSTOM_BYPASS)
REQUIREMENTS = ['total_connect_client==0.16']
@@ -76,6 +78,8 @@ class TotalConnect(alarm.AlarmControlPanel):
state = STATE_ALARM_ARMED_AWAY
elif status == self._client.ARMED_STAY_NIGHT:
state = STATE_ALARM_ARMED_NIGHT
elif status == self._client.ARMED_CUSTOM_BYPASS:
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
elif status == self._client.ARMING:
state = STATE_ALARM_ARMING
elif status == self._client.DISARMING:
+17 -34
View File
@@ -4,16 +4,13 @@ Support for AlarmDecoder devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alarmdecoder/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['alarmdecoder==0.12.3']
@@ -71,9 +68,9 @@ ZONE_SCHEMA = vol.Schema({
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA,
DEVICE_SERIAL_SCHEMA,
DEVICE_USB_SCHEMA),
vol.Required(CONF_DEVICE): vol.Any(
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
DEVICE_USB_SCHEMA),
vol.Optional(CONF_PANEL_DISPLAY,
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
@@ -81,8 +78,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
def setup(hass, config):
"""Set up for the AlarmDecoder devices."""
from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
@@ -99,32 +95,25 @@ def async_setup(hass, config):
path = DEFAULT_DEVICE_PATH
baud = DEFAULT_DEVICE_BAUD
sync_connect = asyncio.Future(loop=hass.loop)
def handle_open(device):
"""Handle the successful connection."""
_LOGGER.info("Established a connection with the alarmdecoder")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
sync_connect.set_result(True)
@callback
def stop_alarmdecoder(event):
"""Handle the shutdown of AlarmDecoder."""
_LOGGER.debug("Shutting down alarmdecoder")
controller.close()
@callback
def handle_message(sender, message):
"""Handle message from AlarmDecoder."""
async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message)
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_PANEL_MESSAGE, message)
def zone_fault_callback(sender, zone):
"""Handle zone fault from AlarmDecoder."""
async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone)
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_ZONE_FAULT, zone)
def zone_restore_callback(sender, zone):
"""Handle zone restore from AlarmDecoder."""
async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone)
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_ZONE_RESTORE, zone)
controller = False
if device_type == 'socket':
@@ -139,7 +128,6 @@ def async_setup(hass, config):
AlarmDecoder(USBDevice.find())
return False
controller.on_open += handle_open
controller.on_message += handle_message
controller.on_zone_fault += zone_fault_callback
controller.on_zone_restore += zone_restore_callback
@@ -148,21 +136,16 @@ def async_setup(hass, config):
controller.open(baud)
result = yield from sync_connect
_LOGGER.debug("Established a connection with the alarmdecoder")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
if not result:
return False
hass.async_add_job(
async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf,
config))
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
if zones:
hass.async_add_job(async_load_platform(
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config))
load_platform(
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
if display:
hass.async_add_job(async_load_platform(
hass, 'sensor', DOMAIN, conf, config))
load_platform(hass, 'sensor', DOMAIN, conf, config)
return True
+1 -1
View File
@@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
from homeassistant.components.discovery import SERVICE_APPLE_TV
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.8']
REQUIREMENTS = ['pyatv==0.3.9']
_LOGGER = logging.getLogger(__name__)
@@ -34,6 +34,7 @@ DEVICE_CLASSES = [
'plug', # On means plugged in, Off means unplugged
'power', # Power, over-current, etc
'presence', # On means home, Off means away
'problem', # On means there is a problem, Off means the status is OK
'safety', # Generic on=unsafe, off=safe
'smoke', # Smoke detector
'sound', # On means sound detected, Off means no sound
@@ -0,0 +1,87 @@
"""
Support for ADS binary sensors.
For more details about this platform, please refer to the documentation.
https://home-assistant.io/components/binary_sensor.ads/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import BinarySensorDevice, \
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA
from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR
from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['ads']
DEFAULT_NAME = 'ADS binary sensor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Binary Sensor platform for ADS."""
ads_hub = hass.data.get(DATA_ADS)
ads_var = config.get(CONF_ADS_VAR)
name = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS)
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
add_devices([ads_sensor])
class AdsBinarySensor(BinarySensorDevice):
"""Representation of ADS binary sensors."""
def __init__(self, ads_hub, name, ads_var, device_class):
"""Initialize AdsBinarySensor entity."""
self._name = name
self._state = False
self._device_class = device_class or 'moving'
self._ads_hub = ads_hub
self.ads_var = ads_var
@asyncio.coroutine
def async_added_to_hass(self):
"""Register device notification."""
def update(name, value):
"""Handle device notifications."""
_LOGGER.debug('Variable %s changed its value to %d',
name, value)
self._state = value
self.schedule_update_ha_state()
self.hass.async_add_job(
self._ads_hub.add_device_notification,
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update
)
@property
def name(self):
"""Return the default name of the binary sensor."""
return self._name
@property
def device_class(self):
"""Return the device class."""
return self._device_class
@property
def is_on(self):
"""Return if the binary sensor is on."""
return self._state
@property
def should_poll(self):
"""Return False because entity pushes its state to HA."""
return False
@@ -7,39 +7,29 @@ https://home-assistant.io/components/binary_sensor.alarmdecoder/
import asyncio
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.alarmdecoder import (ZONE_SCHEMA,
CONF_ZONES,
CONF_ZONE_NAME,
CONF_ZONE_TYPE,
SIGNAL_ZONE_FAULT,
SIGNAL_ZONE_RESTORE)
from homeassistant.components.alarmdecoder import (
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE)
DEPENDENCIES = ['alarmdecoder']
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the AlarmDecoder binary sensor devices."""
configured_zones = discovery_info[CONF_ZONES]
devices = []
for zone_num in configured_zones:
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME]
device = AlarmDecoderBinarySensor(
hass, zone_num, zone_name, zone_type)
device = AlarmDecoderBinarySensor(zone_num, zone_name, zone_type)
devices.append(device)
async_add_devices(devices)
add_devices(devices)
return True
@@ -47,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor."""
def __init__(self, hass, zone_number, zone_name, zone_type):
def __init__(self, zone_number, zone_name, zone_type):
"""Initialize the binary_sensor."""
self._zone_number = zone_number
self._zone_type = zone_type
@@ -55,16 +45,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self._name = zone_name
self._type = zone_type
_LOGGER.debug("Setup up zone: %s", self._name)
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_FAULT, self._fault_callback)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_ZONE_FAULT, self._fault_callback)
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_ZONE_RESTORE, self._restore_callback)
@property
def name(self):
@@ -97,16 +85,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
@callback
def _fault_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self._state = 1
self.async_schedule_update_ha_state()
self.schedule_update_ha_state()
@callback
def _restore_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self._state = 0
self.async_schedule_update_ha_state()
self.schedule_update_ha_state()
+318 -17
View File
@@ -4,24 +4,31 @@ Support for ISY994 binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.isy994/
"""
import asyncio
import logging
from datetime import timedelta
from typing import Callable # noqa
from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
False: STATE_OFF,
True: STATE_ON,
}
UOM = ['2', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
ISY_DEVICE_TYPES = {
'moisture': ['16.8', '16.13', '16.14'],
'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
'motion': ['16.1', '16.4', '16.5', '16.3']
}
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
@@ -32,10 +39,46 @@ def setup_platform(hass, config: ConfigType,
return False
devices = []
devices_by_nid = {}
child_nodes = []
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
states=STATES):
devices.append(ISYBinarySensorDevice(node))
if node.parent_node is None:
device = ISYBinarySensorDevice(node)
devices.append(device)
devices_by_nid[node.nid] = device
else:
# We'll process the child nodes last, to ensure all parent nodes
# have been processed
child_nodes.append(node)
for node in child_nodes:
try:
parent_device = devices_by_nid[node.parent_node.nid]
except KeyError:
_LOGGER.error("Node %s has a parent node %s, but no device "
"was created for the parent. Skipping.",
node.nid, node.parent_nid)
else:
device_type = _detect_device_type(node)
if device_type in ['moisture', 'opening']:
subnode_id = int(node.nid[-1])
# Leak and door/window sensors work the same way with negative
# nodes and heartbeat nodes
if subnode_id == 4:
# Subnode 4 is the heartbeat node, which we will represent
# as a separate binary_sensor
device = ISYBinarySensorHeartbeat(node, parent_device)
parent_device.add_heartbeat_device(device)
devices.append(device)
elif subnode_id == 2:
parent_device.add_negative_node(node)
else:
# We don't yet have any special logic for other sensor types,
# so add the nodes as individual devices
device = ISYBinarySensorDevice(node)
devices.append(device)
for program in isy.PROGRAMS.get(DOMAIN, []):
try:
@@ -48,23 +91,281 @@ def setup_platform(hass, config: ConfigType,
add_devices(devices)
def _detect_device_type(node) -> str:
try:
device_type = node.type
except AttributeError:
# The type attribute didn't exist in the ISY's API response
return None
split_type = device_type.split('.')
for device_class, ids in ISY_DEVICE_TYPES.items():
if '{}.{}'.format(split_type[0], split_type[1]) in ids:
return device_class
return None
def _is_val_unknown(val):
"""Determine if a number value represents UNKNOWN from PyISY."""
return val == -1*float('inf')
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor device."""
"""Representation of an ISY994 binary sensor device.
Often times, a single device is represented by multiple nodes in the ISY,
allowing for different nuances in how those devices report their on and
off events. This class turns those multiple nodes in to a single Hass
entity and handles both ways that ISY binary sensors can work.
"""
def __init__(self, node) -> None:
"""Initialize the ISY994 binary sensor device."""
isy.ISYDevice.__init__(self, node)
super().__init__(node)
self._negative_node = None
self._heartbeat_device = None
self._device_class_from_type = _detect_device_type(self._node)
# pylint: disable=protected-access
if _is_val_unknown(self._node.status._val):
self._computed_state = None
else:
self._computed_state = bool(self._node.status._val)
@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Subscribe to the node and subnode event emitters."""
yield from super().async_added_to_hass()
self._node.controlEvents.subscribe(self._positive_node_control_handler)
if self._negative_node is not None:
self._negative_node.controlEvents.subscribe(
self._negative_node_control_handler)
def add_heartbeat_device(self, device) -> None:
"""Register a heartbeat device for this sensor.
The heartbeat node beats on its own, but we can gain a little
reliability by considering any node activity for this sensor
to be a heartbeat as well.
"""
self._heartbeat_device = device
def _heartbeat(self) -> None:
"""Send a heartbeat to our heartbeat device, if we have one."""
if self._heartbeat_device is not None:
self._heartbeat_device.heartbeat()
def add_negative_node(self, child) -> None:
"""Add a negative node to this binary sensor device.
The negative node is a node that can receive the 'off' events
for the sensor, depending on device configuration and type.
"""
self._negative_node = child
if not _is_val_unknown(self._negative_node):
# If the negative node has a value, it means the negative node is
# in use for this device. Therefore, we cannot determine the state
# of the sensor until we receive our first ON event.
self._computed_state = None
def _negative_node_control_handler(self, event: object) -> None:
"""Handle an "On" control event from the "negative" node."""
if event == 'DON':
_LOGGER.debug("Sensor %s turning Off via the Negative node "
"sending a DON command", self.name)
self._computed_state = False
self.schedule_update_ha_state()
self._heartbeat()
def _positive_node_control_handler(self, event: object) -> None:
"""Handle On and Off control event coming from the primary node.
Depending on device configuration, sometimes only On events
will come to this node, with the negative node representing Off
events
"""
if event == 'DON':
_LOGGER.debug("Sensor %s turning On via the Primary node "
"sending a DON command", self.name)
self._computed_state = True
self.schedule_update_ha_state()
self._heartbeat()
if event == 'DOF':
_LOGGER.debug("Sensor %s turning Off via the Primary node "
"sending a DOF command", self.name)
self._computed_state = False
self.schedule_update_ha_state()
self._heartbeat()
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Ignore primary node status updates.
We listen directly to the Control events on all nodes for this
device.
"""
pass
@property
def value(self) -> object:
"""Get the current value of the device.
Insteon leak sensors set their primary node to On when the state is
DRY, not WET, so we invert the binary state if the user indicates
that it is a moisture sensor.
"""
if self._computed_state is None:
# Do this first so we don't invert None on moisture sensors
return None
if self.device_class == 'moisture':
return not self._computed_state
return self._computed_state
@property
def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on.
Note: This method will return false if the current state is UNKNOWN
"""
return bool(self.value)
@property
def state(self):
"""Return the state of the binary sensor."""
if self._computed_state is None:
return None
return STATE_ON if self.is_on else STATE_OFF
@property
def device_class(self) -> str:
"""Return the class of this device.
This was discovered by parsing the device type code during init
"""
return self._device_class_from_type
class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
"""Representation of the battery state of an ISY994 sensor."""
def __init__(self, node, parent_device) -> None:
"""Initialize the ISY994 binary sensor device."""
super().__init__(node)
self._computed_state = None
self._parent_device = parent_device
self._heartbeat_timer = None
@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Subscribe to the node and subnode event emitters."""
yield from super().async_added_to_hass()
self._node.controlEvents.subscribe(
self._heartbeat_node_control_handler)
# Start the timer on bootup, so we can change from UNKNOWN to ON
self._restart_timer()
def _heartbeat_node_control_handler(self, event: object) -> None:
"""Update the heartbeat timestamp when an On event is sent."""
if event == 'DON':
self.heartbeat()
def heartbeat(self):
"""Mark the device as online, and restart the 25 hour timer.
This gets called when the heartbeat node beats, but also when the
parent sensor sends any events, as we can trust that to mean the device
is online. This mitigates the risk of false positives due to a single
missed heartbeat event.
"""
self._computed_state = False
self._restart_timer()
self.schedule_update_ha_state()
def _restart_timer(self):
"""Restart the 25 hour timer."""
try:
self._heartbeat_timer()
self._heartbeat_timer = None
except TypeError:
# No heartbeat timer is active
pass
# pylint: disable=unused-argument
@callback
def timer_elapsed(now) -> None:
"""Heartbeat missed; set state to indicate dead battery."""
self._computed_state = True
self._heartbeat_timer = None
self.schedule_update_ha_state()
point_in_time = dt_util.utcnow() + timedelta(hours=25)
_LOGGER.debug("Timer starting. Now: %s Then: %s",
dt_util.utcnow(), point_in_time)
self._heartbeat_timer = async_track_point_in_utc_time(
self.hass, timer_elapsed, point_in_time)
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Ignore node status updates.
We listen directly to the Control events for this device.
"""
pass
@property
def value(self) -> object:
"""Get the current value of this sensor."""
return self._computed_state
@property
def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on.
Note: This method will return false if the current state is UNKNOWN
"""
return bool(self.value)
@property
def state(self):
"""Return the state of the binary sensor."""
if self._computed_state is None:
return None
return STATE_ON if self.is_on else STATE_OFF
@property
def device_class(self) -> str:
"""Get the class of this device."""
return 'battery'
@property
def device_state_attributes(self):
"""Get the state attributes for the device."""
attr = super().device_state_attributes
attr['parent_entity_id'] = self._parent_device.entity_id
return attr
class ISYBinarySensorProgram(isy.ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor program.
This does not need all of the subnode logic in the device version of binary
sensors.
"""
def __init__(self, name, node) -> None:
"""Initialize the ISY994 binary sensor program."""
super().__init__(node)
self._name = name
@property
def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on."""
return bool(self.value)
class ISYBinarySensorProgram(ISYBinarySensorDevice):
"""Representation of an ISY994 binary sensor program."""
def __init__(self, name, node) -> None:
"""Initialize the ISY994 binary sensor program."""
ISYBinarySensorDevice.__init__(self, node)
self._name = name
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Perform the setup for Vera controller devices."""
add_devices(
VeraBinarySensor(device, VERA_CONTROLLER)
for device in VERA_DEVICES['binary_sensor'])
VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
for device in hass.data[VERA_DEVICES]['binary_sensor'])
class VeraBinarySensor(VeraDevice, BinarySensorDevice):
+230
View File
@@ -0,0 +1,230 @@
"""
Support for WebDav Calendar.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/calendar.caldav/
"""
import logging
import re
from datetime import datetime, timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.calendar import (
CalendarEventDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
from homeassistant.util import dt, Throttle
REQUIREMENTS = ['caldav==0.5.0']
_LOGGER = logging.getLogger(__name__)
CONF_DEVICE_ID = 'device_id'
CONF_CALENDARS = 'calendars'
CONF_CUSTOM_CALENDARS = 'custom_calendars'
CONF_CALENDAR = 'calendar'
CONF_ALL_DAY = 'all_day'
CONF_SEARCH = 'search'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): vol.Url,
vol.Optional(CONF_CALENDARS, default=[]):
vol.All(cv.ensure_list, vol.Schema([
cv.string
])),
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
vol.Optional(CONF_CUSTOM_CALENDARS, default=[]):
vol.All(cv.ensure_list, vol.Schema([
vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_CALENDAR): cv.string,
vol.Required(CONF_SEARCH): cv.string
})
]))
})
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, disc_info=None):
"""Set up the WebDav Calendar platform."""
import caldav
client = caldav.DAVClient(config.get(CONF_URL),
None,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
# Retrieve all the remote calendars
calendars = client.principal().calendars()
calendar_devices = []
for calendar in list(calendars):
# If a calendar name was given in the configuration,
# ignore all the others
if (config.get(CONF_CALENDARS)
and calendar.name not in config.get(CONF_CALENDARS)):
_LOGGER.debug("Ignoring calendar '%s'", calendar.name)
continue
# Create additional calendars based on custom filtering
# rules
for cust_calendar in config.get(CONF_CUSTOM_CALENDARS):
# Check that the base calendar matches
if cust_calendar.get(CONF_CALENDAR) != calendar.name:
continue
device_data = {
CONF_NAME: cust_calendar.get(CONF_NAME),
CONF_DEVICE_ID: "{} {}".format(
cust_calendar.get(CONF_CALENDAR),
cust_calendar.get(CONF_NAME)),
}
calendar_devices.append(
WebDavCalendarEventDevice(hass,
device_data,
calendar,
cust_calendar.get(CONF_ALL_DAY),
cust_calendar.get(CONF_SEARCH))
)
# Create a default calendar if there was no custom one
if not config.get(CONF_CUSTOM_CALENDARS):
device_data = {
CONF_NAME: calendar.name,
CONF_DEVICE_ID: calendar.name
}
calendar_devices.append(
WebDavCalendarEventDevice(hass, device_data, calendar)
)
# Finally add all the calendars we've created
add_devices(calendar_devices)
class WebDavCalendarEventDevice(CalendarEventDevice):
"""A device for getting the next Task from a WebDav Calendar."""
def __init__(self,
hass,
device_data,
calendar,
all_day=False,
search=None):
"""Create the WebDav Calendar Event Device."""
self.data = WebDavCalendarData(calendar, all_day, search)
super().__init__(hass, device_data)
@property
def device_state_attributes(self):
"""Return the device state attributes."""
if self.data.event is None:
# No tasks, we don't REALLY need to show anything.
return {}
attributes = super().device_state_attributes
return attributes
class WebDavCalendarData(object):
"""Class to utilize the calendar dav client object to get next event."""
def __init__(self, calendar, include_all_day, search):
"""Set up how we are going to search the WebDav calendar."""
self.calendar = calendar
self.include_all_day = include_all_day
self.search = search
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
# We have to retrieve the results for the whole day as the server
# won't return events that have already started
results = self.calendar.date_search(
dt.start_of_local_day(),
dt.start_of_local_day() + timedelta(days=1)
)
# dtstart can be a date or datetime depending if the event lasts a
# whole day. Convert everything to datetime to be able to sort it
results.sort(key=lambda x: self.to_datetime(
x.instance.vevent.dtstart.value
))
vevent = next((
event.instance.vevent for event in results
if (self.is_matching(event.instance.vevent, self.search)
and (not self.is_all_day(event.instance.vevent)
or self.include_all_day)
and not self.is_over(event.instance.vevent))), None)
# If no matching event could be found
if vevent is None:
_LOGGER.debug(
"No matching event found in the %d results for %s",
len(results),
self.calendar.name,
)
self.event = None
return True
# Populate the entity attributes with the event values
self.event = {
"summary": vevent.summary.value,
"start": self.get_hass_date(vevent.dtstart.value),
"end": self.get_hass_date(vevent.dtend.value),
"location": self.get_attr_value(vevent, "location"),
"description": self.get_attr_value(vevent, "description")
}
return True
@staticmethod
def is_matching(vevent, search):
"""Return if the event matches the filter critera."""
if search is None:
return True
pattern = re.compile(search)
return (hasattr(vevent, "summary")
and pattern.match(vevent.summary.value)
or hasattr(vevent, "location")
and pattern.match(vevent.location.value)
or hasattr(vevent, "description")
and pattern.match(vevent.description.value))
@staticmethod
def is_all_day(vevent):
"""Return if the event last the whole day."""
return not isinstance(vevent.dtstart.value, datetime)
@staticmethod
def is_over(vevent):
"""Return if the event is over."""
return dt.now() > WebDavCalendarData.to_datetime(vevent.dtend.value)
@staticmethod
def get_hass_date(obj):
"""Return if the event matches."""
if isinstance(obj, datetime):
return {"dateTime": obj.isoformat()}
return {"date": obj.isoformat()}
@staticmethod
def to_datetime(obj):
"""Return a datetime."""
if isinstance(obj, datetime):
return obj
return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
@staticmethod
def get_attr_value(obj, attribute):
"""Return the value of the attribute if defined."""
if hasattr(obj, attribute):
return getattr(obj, attribute).value
return None
+95
View File
@@ -0,0 +1,95 @@
"""
Support for Canary camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.canary/
"""
import logging
import requests
from homeassistant.components.camera import Camera
from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
DEPENDENCIES = ['canary']
_LOGGER = logging.getLogger(__name__)
ATTR_MOTION_START_TIME = "motion_start_time"
ATTR_MOTION_END_TIME = "motion_end_time"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Canary sensors."""
data = hass.data[DATA_CANARY]
devices = []
for location in data.locations:
entries = data.get_motion_entries(location.location_id)
if entries:
devices.append(CanaryCamera(data, location.location_id,
DEFAULT_TIMEOUT))
add_devices(devices, True)
class CanaryCamera(Camera):
"""An implementation of a Canary security camera."""
def __init__(self, data, location_id, timeout):
"""Initialize a Canary security camera."""
super().__init__()
self._data = data
self._location_id = location_id
self._timeout = timeout
self._location = None
self._motion_entry = None
self._image_content = None
def camera_image(self):
"""Update the status of the camera and return bytes of camera image."""
self.update()
return self._image_content
@property
def name(self):
"""Return the name of this device."""
return self._location.name
@property
def is_recording(self):
"""Return true if the device is recording."""
return self._location.is_recording
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
if self._motion_entry is None:
return None
return {
ATTR_MOTION_START_TIME: self._motion_entry.start_time,
ATTR_MOTION_END_TIME: self._motion_entry.end_time,
}
def update(self):
"""Update the status of the camera."""
self._data.update()
self._location = self._data.get_location(self._location_id)
entries = self._data.get_motion_entries(self._location_id)
if entries:
current = entries[0]
previous = self._motion_entry
if previous is None or previous.entry_id != current.entry_id:
self._motion_entry = current
self._image_content = requests.get(
current.thumbnails[0].image_url,
timeout=self._timeout).content
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return not self._location.is_recording
+117
View File
@@ -0,0 +1,117 @@
"""
Support for Canary.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/canary/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from requests import ConnectTimeout, HTTPError
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
REQUIREMENTS = ['py-canary==0.2.3']
_LOGGER = logging.getLogger(__name__)
NOTIFICATION_ID = 'canary_notification'
NOTIFICATION_TITLE = 'Canary Setup'
DOMAIN = 'canary'
DATA_CANARY = 'canary'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
DEFAULT_TIMEOUT = 10
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}),
}, extra=vol.ALLOW_EXTRA)
CANARY_COMPONENTS = [
'alarm_control_panel', 'camera', 'sensor'
]
def setup(hass, config):
"""Set up the Canary component."""
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
timeout = conf.get(CONF_TIMEOUT)
try:
hass.data[DATA_CANARY] = CanaryData(username, password, timeout)
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Canary service: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
for component in CANARY_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
class CanaryData(object):
"""Get the latest data and update the states."""
def __init__(self, username, password, timeout):
"""Init the Canary data object."""
from canary.api import Api
self._api = Api(username, password, timeout)
self._locations_by_id = {}
self._readings_by_device_id = {}
self._entries_by_location_id = {}
self.update()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, **kwargs):
"""Get the latest data from py-canary."""
for location in self._api.get_locations():
location_id = location.location_id
self._locations_by_id[location_id] = location
self._entries_by_location_id[location_id] = self._api.get_entries(
location_id, entry_type="motion", limit=1)
for device in location.devices:
if device.is_online:
self._readings_by_device_id[device.device_id] = \
self._api.get_latest_readings(device.device_id)
@property
def locations(self):
"""Return a list of locations."""
return self._locations_by_id.values()
def get_motion_entries(self, location_id):
"""Return a list of motion entries based on location_id."""
return self._entries_by_location_id.get(location_id, [])
def get_location(self, location_id):
"""Return a location based on location_id."""
return self._locations_by_id.get(location_id, [])
def get_readings(self, device_id):
"""Return a list of readings based on device_id."""
return self._readings_by_device_id.get(device_id, [])
def set_location_mode(self, location_id, mode_name, is_private=False):
"""Set location mode."""
self._api.set_location_mode(location_id, mode_name, is_private)
self.update(no_throttle=True)
@@ -13,7 +13,8 @@ from homeassistant.core import callback
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
STATE_AUTO, ATTR_OPERATION_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
@@ -40,7 +41,7 @@ CONF_MIN_DUR = 'min_cycle_duration'
CONF_COLD_TOLERANCE = 'cold_tolerance'
CONF_HOT_TOLERANCE = 'hot_tolerance'
CONF_KEEP_ALIVE = 'keep_alive'
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -58,6 +59,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
vol.Optional(CONF_KEEP_ALIVE): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_INITIAL_OPERATION_MODE):
vol.In([STATE_AUTO, STATE_OFF])
})
@@ -75,11 +78,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
cold_tolerance = config.get(CONF_COLD_TOLERANCE)
hot_tolerance = config.get(CONF_HOT_TOLERANCE)
keep_alive = config.get(CONF_KEEP_ALIVE)
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
async_add_devices([GenericThermostat(
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
target_temp, ac_mode, min_cycle_duration, cold_tolerance,
hot_tolerance, keep_alive)])
hot_tolerance, keep_alive, initial_operation_mode)])
class GenericThermostat(ClimateDevice):
@@ -87,7 +91,8 @@ class GenericThermostat(ClimateDevice):
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
cold_tolerance, hot_tolerance, keep_alive):
cold_tolerance, hot_tolerance, keep_alive,
initial_operation_mode):
"""Initialize the thermostat."""
self.hass = hass
self._name = name
@@ -97,7 +102,11 @@ class GenericThermostat(ClimateDevice):
self._cold_tolerance = cold_tolerance
self._hot_tolerance = hot_tolerance
self._keep_alive = keep_alive
self._enabled = True
self._initial_operation_mode = initial_operation_mode
if initial_operation_mode == STATE_OFF:
self._enabled = False
else:
self._enabled = True
self._active = False
self._cur_temp = None
@@ -122,14 +131,20 @@ class GenericThermostat(ClimateDevice):
@asyncio.coroutine
def async_added_to_hass(self):
"""Run when entity about to be added."""
# If we have an old state and no target temp, restore
if self._target_temp is None:
old_state = yield from async_get_last_state(self.hass,
self.entity_id)
if old_state is not None:
# Check If we have an old state
old_state = yield from async_get_last_state(self.hass,
self.entity_id)
if old_state is not None:
# If we have no initial temperature, restore
if self._target_temp is None:
self._target_temp = float(
old_state.attributes[ATTR_TEMPERATURE])
# If we have no initial operation mode, restore
if self._initial_operation_mode is None:
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
self._enabled = False
@property
def should_poll(self):
"""Return the polling state."""
+9 -3
View File
@@ -59,8 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
climate_devices = []
for zone in zones:
climate_devices.append(create_climate_device(
tado, hass, zone, zone['name'], zone['id']))
device = create_climate_device(
tado, hass, zone, zone['name'], zone['id'])
if not device:
continue
climate_devices.append(device)
if climate_devices:
add_devices(climate_devices, True)
@@ -75,8 +78,11 @@ def create_climate_device(tado, hass, zone, name, zone_id):
if ac_mode:
temperatures = capabilities['HEAT']['temperatures']
else:
elif 'temperatures' in capabilities:
temperatures = capabilities['temperatures']
else:
_LOGGER.debug("Received zone %s has no temperature; not adding", name)
return
min_temp = float(temperatures['celsius']['min'])
max_temp = float(temperatures['celsius']['max'])
+2 -2
View File
@@ -32,8 +32,8 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up of Vera thermostats."""
add_devices_callback(
VeraThermostat(device, VERA_CONTROLLER) for
device in VERA_DEVICES['climate'])
VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['climate'])
class VeraThermostat(VeraDevice, ClimateDevice):
+6 -10
View File
@@ -16,7 +16,7 @@ from homeassistant.components.alexa import smart_home
from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.5.0']
REQUIREMENTS = ['warrant==0.6.1']
_LOGGER = logging.getLogger(__name__)
@@ -27,7 +27,7 @@ CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
MODE_DEV = 'development'
DEFAULT_MODE = MODE_DEV
DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
ALEXA_SCHEMA = vol.Schema({
@@ -42,10 +42,10 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV] + list(SERVERS)),
# Change to optional when we include real servers
vol.Required(CONF_COGNITO_CLIENT_ID): str,
vol.Required(CONF_USER_POOL_ID): str,
vol.Required(CONF_REGION): str,
vol.Required(CONF_RELAYER): str,
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
}),
}, extra=vol.ALLOW_EXTRA)
@@ -117,10 +117,6 @@ class Cloud:
@property
def subscription_expired(self):
"""Return a boolen if the subscription has expired."""
# For now, don't enforce subscriptions to exist
if 'custom:sub-exp' not in self.claims:
return False
return dt_util.utcnow() > self.expiration_date
@property
+5 -2
View File
@@ -68,11 +68,14 @@ def register(cloud, email, password):
from botocore.exceptions import ClientError
cognito = _cognito(cloud)
# Workaround for bug in Warrant. PR with fix:
# https://github.com/capless/warrant/pull/82
cognito.add_base_attributes()
try:
if cloud.cognito_email_based:
cognito.register(email, password, email=email)
cognito.register(email, password)
else:
cognito.register(_generate_username(email), password, email=email)
cognito.register(_generate_username(email), password)
except ClientError as err:
raise _map_aws_exception(err)
+6 -7
View File
@@ -4,13 +4,12 @@ CONFIG_DIR = '.cloud'
REQUEST_TIMEOUT = 10
SERVERS = {
# Example entry:
# 'production': {
# 'cognito_client_id': '',
# 'user_pool_id': '',
# 'region': '',
# 'relayer': ''
# }
'production': {
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
'user_pool_id': 'us-east-1_87ll5WOP8',
'region': 'us-east-1',
'relayer': 'wss://cloud.hass.io:8000/websocket'
}
}
MESSAGE_EXPIRATION = """
@@ -1,4 +1,4 @@
"""Provide configuration end points for Z-Wave."""
"""Provide configuration end points for Automations."""
import asyncio
from homeassistant.components.config import EditIdBasedConfigView
+4 -1
View File
@@ -69,7 +69,10 @@ class ISYCoverDevice(isy.ISYDevice, CoverDevice):
@property
def state(self) -> str:
"""Get the state of the ISY994 cover device."""
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
if self.is_unknown():
return None
else:
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
def open_cover(self, **kwargs) -> None:
"""Send the open cover command to the ISY994 cover device."""
+65
View File
@@ -0,0 +1,65 @@
"""
Support for Tellstick covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tellstick/
"""
from homeassistant.components.cover import CoverDevice
from homeassistant.components.tellstick import (
DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG,
DATA_TELLSTICK, TellstickDevice)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tellstick covers."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None):
return
signal_repetitions = discovery_info.get(
ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS)
add_devices([TellstickCover(hass.data[DATA_TELLSTICK][tellcore_id],
signal_repetitions)
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]],
True)
class TellstickCover(TellstickDevice, CoverDevice):
"""Representation of a Tellstick cover."""
@property
def is_closed(self):
"""Return the current position of the cover is not possible."""
return None
@property
def assumed_state(self):
"""Return True if unable to access real state of the entity."""
return True
def close_cover(self, **kwargs):
"""Close the cover."""
self._tellcore_device.down()
def open_cover(self, **kwargs):
"""Open the cover."""
self._tellcore_device.up()
def stop_cover(self, **kwargs):
"""Stop the cover."""
self._tellcore_device.stop()
def _parse_tellcore_data(self, tellcore_data):
"""Turn the value received from tellcore into something useful."""
pass
def _parse_ha_data(self, kwargs):
"""Turn the value from HA into something useful."""
pass
def _update_model(self, new_state, data):
"""Update the device entity state to match the arguments."""
pass
+2 -2
View File
@@ -18,8 +18,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera covers."""
add_devices(
VeraCover(device, VERA_CONTROLLER) for
device in VERA_DEVICES['cover'])
VeraCover(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['cover'])
class VeraCover(VeraDevice, CoverDevice):
@@ -53,6 +53,7 @@ YAML_DEVICES = 'known_devices.yaml'
CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
@@ -81,12 +82,18 @@ ATTR_VENDOR = 'vendor'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
}))
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME,
default=DEFAULT_CONSIDER_HOME): vol.All(
cv.time_period, cv.positive_timedelta)
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_NEW_DEVICE_DEFAULTS,
default={}): NEW_DEVICE_DEFAULTS_SCHEMA
})
@@ -125,9 +132,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
conf = conf[0] if conf else {}
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
devices = yield from async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(hass, consider_home, track_new, devices)
tracker = DeviceTracker(
hass, consider_home, track_new, defaults, devices)
@asyncio.coroutine
def async_setup_platform(p_type, p_config, disc_info=None):
@@ -211,13 +220,15 @@ class DeviceTracker(object):
"""Representation of a device tracker."""
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track_new: bool, devices: Sequence) -> None:
track_new: bool, defaults: dict,
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}
self.consider_home = consider_home
self.track_new = track_new
self.track_new = defaults.get(CONF_TRACK_NEW, track_new)
self.defaults = defaults
self.group = None
self._is_updating = asyncio.Lock(loop=hass.loop)
@@ -274,7 +285,8 @@ class DeviceTracker(object):
device = Device(
self.hass, self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '),
picture=picture, icon=icon)
picture=picture, icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
self.devices[dev_id] = device
if mac is not None:
self.mac_to_dev[mac] = device
@@ -11,7 +11,8 @@ import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL)
@@ -38,7 +39,7 @@ def get_scanner(hass, config):
return None
class LinksysAPDeviceScanner(object):
class LinksysAPDeviceScanner(DeviceScanner):
"""This class queries a Linksys Access Point."""
def __init__(self, config):
@@ -0,0 +1,136 @@
"""
Support for the Meraki CMX location service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.meraki/
"""
import asyncio
import logging
import json
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY)
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER)
CONF_VALIDATOR = 'validator'
CONF_SECRET = 'secret'
DEPENDENCIES = ['http']
URL = '/api/meraki'
VERSION = '2.0'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_VALIDATOR): cv.string,
vol.Required(CONF_SECRET): cv.string
})
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the Meraki tracker."""
hass.http.register_view(
MerakiView(config, async_see))
return True
class MerakiView(HomeAssistantView):
"""View to handle Meraki requests."""
url = URL
name = 'api:meraki'
def __init__(self, config, async_see):
"""Initialize Meraki URL endpoints."""
self.async_see = async_see
self.validator = config[CONF_VALIDATOR]
self.secret = config[CONF_SECRET]
@asyncio.coroutine
def get(self, request):
"""Meraki message received as GET."""
return self.validator
@asyncio.coroutine
def post(self, request):
"""Meraki CMX message received."""
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
_LOGGER.debug("Meraki Data from Post: %s", json.dumps(data))
if not data.get('secret', False):
_LOGGER.error("secret invalid")
return self.json_message('No secret', HTTP_UNPROCESSABLE_ENTITY)
if data['secret'] != self.secret:
_LOGGER.error("Invalid Secret received from Meraki")
return self.json_message('Invalid secret',
HTTP_UNPROCESSABLE_ENTITY)
elif data['version'] != VERSION:
_LOGGER.error("Invalid API version: %s", data['version'])
return self.json_message('Invalid version',
HTTP_UNPROCESSABLE_ENTITY)
else:
_LOGGER.debug('Valid Secret')
if data['type'] not in ('DevicesSeen', 'BluetoothDevicesSeen'):
_LOGGER.error("Unknown Device %s", data['type'])
return self.json_message('Invalid device type',
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.debug("Processing %s", data['type'])
if len(data["data"]["observations"]) == 0:
_LOGGER.debug("No observations found")
return
self._handle(request.app['hass'], data)
@callback
def _handle(self, hass, data):
for i in data["data"]["observations"]:
data["data"]["secret"] = "hidden"
lat = i["location"]["lat"]
lng = i["location"]["lng"]
try:
accuracy = int(float(i["location"]["unc"]))
except ValueError:
accuracy = 0
mac = i["clientMac"]
_LOGGER.debug("clientMac: %s", mac)
if lat == "NaN" or lng == "NaN":
_LOGGER.debug(
"No coordinates received, skipping location for: " + mac
)
gps_location = None
accuracy = None
else:
gps_location = (lat, lng)
attrs = {}
if i.get('os', False):
attrs['os'] = i['os']
if i.get('manufacturer', False):
attrs['manufacturer'] = i['manufacturer']
if i.get('ipv4', False):
attrs['ipv4'] = i['ipv4']
if i.get('ipv6', False):
attrs['ipv6'] = i['ipv6']
if i.get('seenTime', False):
attrs['seenTime'] = i['seenTime']
if i.get('ssid', False):
attrs['ssid'] = i['ssid']
hass.async_add_job(self.async_see(
gps=gps_location,
mac=mac,
source_type=SOURCE_TYPE_ROUTER,
gps_accuracy=accuracy,
attributes=attrs
))
+2 -1
View File
@@ -36,6 +36,7 @@ SERVICE_APPLE_TV = 'apple_tv'
SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
@@ -48,7 +49,7 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
'philips_hue': ('light', 'hue'),
SERVICE_HUE: ('hue', None),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),
+35 -21
View File
@@ -58,7 +58,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(ATTR_PHONE): cv.string,
vol.Required(ATTR_ADDRESS): cv.string,
vol.Optional(ATTR_SHOW_MENU): cv.boolean,
vol.Optional(ATTR_ORDERS): vol.All(cv.ensure_list, [_ORDERS_SCHEMA]),
vol.Optional(ATTR_ORDERS, default=[]): vol.All(
cv.ensure_list, [_ORDERS_SCHEMA]),
}),
}, extra=vol.ALLOW_EXTRA)
@@ -81,7 +82,8 @@ def setup(hass, config):
order = DominosOrder(order_info, dominos)
entities.append(order)
component.add_entities(entities)
if entities:
component.add_entities(entities)
# Return boolean to indicate that initialization was successfully.
return True
@@ -93,7 +95,8 @@ class Dominos():
def __init__(self, hass, config):
"""Set up main service."""
conf = config[DOMAIN]
from pizzapi import Address, Customer, Store
from pizzapi import Address, Customer
from pizzapi.address import StoreException
self.hass = hass
self.customer = Customer(
conf.get(ATTR_FIRST_NAME),
@@ -105,7 +108,10 @@ class Dominos():
*self.customer.address.split(','),
country=conf.get(ATTR_COUNTRY))
self.country = conf.get(ATTR_COUNTRY)
self.closest_store = Store()
try:
self.closest_store = self.address.closest_store()
except StoreException:
self.closest_store = None
def handle_order(self, call):
"""Handle ordering pizza."""
@@ -123,29 +129,32 @@ class Dominos():
from pizzapi.address import StoreException
try:
self.closest_store = self.address.closest_store()
return True
except StoreException:
self.closest_store = False
self.closest_store = None
return False
def get_menu(self):
"""Return the products from the closest stores menu."""
if self.closest_store is False:
self.update_closest_store()
if self.closest_store is None:
_LOGGER.warning('Cannot get menu. Store may be closed')
return
return []
else:
menu = self.closest_store.get_menu()
product_entries = []
menu = self.closest_store.get_menu()
product_entries = []
for product in menu.products:
item = {}
if isinstance(product.menu_data['Variants'], list):
variants = ', '.join(product.menu_data['Variants'])
else:
variants = product.menu_data['Variants']
item['name'] = product.name
item['variants'] = variants
product_entries.append(item)
for product in menu.products:
item = {}
if isinstance(product.menu_data['Variants'], list):
variants = ', '.join(product.menu_data['Variants'])
else:
variants = product.menu_data['Variants']
item['name'] = product.name
item['variants'] = variants
product_entries.append(item)
return product_entries
return product_entries
class DominosProductListView(http.HomeAssistantView):
@@ -192,7 +201,7 @@ class DominosOrder(Entity):
@property
def state(self):
"""Return the state either closed, orderable or unorderable."""
if self.dominos.closest_store is False:
if self.dominos.closest_store is None:
return 'closed'
else:
return 'orderable' if self._orderable else 'unorderable'
@@ -217,6 +226,11 @@ class DominosOrder(Entity):
def order(self):
"""Create the order object."""
from pizzapi import Order
from pizzapi.address import StoreException
if self.dominos.closest_store is None:
raise StoreException
order = Order(
self.dominos.closest_store,
self.dominos.customer,
+1 -1
View File
@@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle
from homeassistant.util.json import save_json
REQUIREMENTS = ['python-ecobee-api==0.0.12']
REQUIREMENTS = ['python-ecobee-api==0.0.14']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
+12 -7
View File
@@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20171130.0', 'user-agents==1.1.0']
REQUIREMENTS = ['home-assistant-frontend==20171216.0', 'user-agents==1.1.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@@ -35,7 +35,7 @@ CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version'
JS_DEFAULT_OPTION = 'es5'
JS_DEFAULT_OPTION = 'auto'
JS_OPTIONS = ['es5', 'latest', 'auto']
DEFAULT_THEME_COLOR = '#03A9F4'
@@ -299,11 +299,16 @@ def async_setup(hass, config):
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
if is_dev:
hass.http.register_static_path(
"/home-assistant-polymer", repo_path, False)
for subpath in ["src", "build-translations", "build-temp", "build",
"hass_frontend", "bower_components", "panels"]:
hass.http.register_static_path(
"/home-assistant-polymer/{}".format(subpath),
os.path.join(repo_path, subpath),
False)
hass.http.register_static_path(
"/static/translations",
os.path.join(repo_path, "build-translations"), False)
os.path.join(repo_path, "build-translations/output"), False)
sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js")
sw_path_latest = os.path.join(repo_path, "build/service_worker.js")
static_path = os.path.join(repo_path, 'hass_frontend')
@@ -583,9 +588,9 @@ def _is_latest(js_option, request):
family_min_version = {
'Chrome': 50, # Probably can reduce this
'Firefox': 41, # Destructuring added in 41
'Firefox': 43, # Array.protopype.includes added in 43
'Opera': 40, # Probably can reduce this
'Edge': 14, # Maybe can reduce this
'Edge': 14, # Array.protopype.includes added in 14
'Safari': 10, # many features not supported by 9
}
version = family_min_version.get(useragent.browser.family)
@@ -5,25 +5,26 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematic/
"""
import asyncio
import os
import logging
from datetime import timedelta
from functools import partial
import logging
import os
import socket
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD,
CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID)
EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN)
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyhomematic==0.1.35']
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.36']
DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL_HUB = timedelta(seconds=300)
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
@@ -41,9 +42,11 @@ ATTR_CHANNEL = 'channel'
ATTR_NAME = 'name'
ATTR_ADDRESS = 'address'
ATTR_VALUE = 'value'
ATTR_PROXY = 'proxy'
ATTR_INTERFACE = 'interface'
ATTR_ERRORCODE = 'error'
ATTR_MESSAGE = 'message'
ATTR_MODE = 'mode'
ATTR_TIME = 'time'
EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse'
@@ -51,8 +54,9 @@ EVENT_ERROR = 'homematic.error'
SERVICE_VIRTUALKEY = 'virtualkey'
SERVICE_RECONNECT = 'reconnect'
SERVICE_SET_VAR_VALUE = 'set_var_value'
SERVICE_SET_DEV_VALUE = 'set_dev_value'
SERVICE_SET_VARIABLE_VALUE = 'set_variable_value'
SERVICE_SET_DEVICE_VALUE = 'set_device_value'
SERVICE_SET_INSTALL_MODE = 'set_install_mode'
HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [
@@ -90,12 +94,14 @@ HM_ATTRIBUTE_SUPPORT = {
'RSSI_DEVICE': ['rssi', {}],
'VALVE_STATE': ['valve', {}],
'BATTERY_STATE': ['battery', {}],
'CONTROL_MODE': ['mode', {0: 'Auto',
1: 'Manual',
2: 'Away',
3: 'Boost',
4: 'Comfort',
5: 'Lowering'}],
'CONTROL_MODE': ['mode', {
0: 'Auto',
1: 'Manual',
2: 'Away',
3: 'Boost',
4: 'Comfort',
5: 'Lowering'
}],
'POWER': ['power', {}],
'CURRENT': ['current', {}],
'VOLTAGE': ['voltage', {}],
@@ -114,8 +120,6 @@ HM_IMPULSE_EVENTS = [
'SEQUENCE_OK',
]
_LOGGER = logging.getLogger(__name__)
CONF_RESOLVENAMES_OPTIONS = [
'metadata',
'json',
@@ -124,12 +128,12 @@ CONF_RESOLVENAMES_OPTIONS = [
]
DATA_HOMEMATIC = 'homematic'
DATA_DEVINIT = 'homematic_devinit'
DATA_STORE = 'homematic_store'
DATA_CONF = 'homematic_conf'
CONF_INTERFACES = 'interfaces'
CONF_LOCAL_IP = 'local_ip'
CONF_LOCAL_PORT = 'local_port'
CONF_IP = 'ip'
CONF_PORT = 'port'
CONF_PATH = 'path'
CONF_CALLBACK_IP = 'callback_ip'
@@ -146,37 +150,33 @@ DEFAULT_PORT = 2001
DEFAULT_PATH = ''
DEFAULT_USERNAME = 'Admin'
DEFAULT_PASSWORD = ''
DEFAULT_VARIABLES = False
DEFAULT_DEVICES = True
DEFAULT_PRIMARY = False
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'homematic',
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_ADDRESS): cv.string,
vol.Required(ATTR_PROXY): cv.string,
vol.Required(ATTR_INTERFACE): cv.string,
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
vol.Optional(ATTR_PARAM): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOSTS): {cv.match_all: {
vol.Required(CONF_IP): cv.string,
vol.Optional(CONF_INTERFACES, default={}): {cv.match_all: {
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES):
cv.boolean,
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): cv.boolean,
vol.Optional(CONF_PRIMARY, default=DEFAULT_PRIMARY): cv.boolean,
vol.Optional(CONF_CALLBACK_IP): cv.string,
vol.Optional(CONF_CALLBACK_PORT): cv.port,
}},
vol.Optional(CONF_HOSTS, default={}): {cv.match_all: {
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
}},
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
}),
@@ -186,61 +186,88 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): cv.string,
vol.Optional(ATTR_PROXY): cv.string,
vol.Optional(ATTR_INTERFACE): cv.string,
})
SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({
SCHEMA_SERVICE_SET_VARIABLE_VALUE = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.match_all,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({
SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_VALUE): cv.match_all,
vol.Optional(ATTR_PROXY): cv.string,
vol.Optional(ATTR_INTERFACE): cv.string,
})
SCHEMA_SERVICE_RECONNECT = vol.Schema({})
SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({
vol.Required(ATTR_INTERFACE): cv.string,
vol.Optional(ATTR_TIME, default=60): cv.positive_int,
vol.Optional(ATTR_MODE, default=1):
vol.All(vol.Coerce(int), vol.In([1, 2])),
vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
})
def virtualkey(hass, address, channel, param, proxy=None):
@bind_hass
def virtualkey(hass, address, channel, param, interface=None):
"""Send virtual keypress to homematic controlller."""
data = {
ATTR_ADDRESS: address,
ATTR_CHANNEL: channel,
ATTR_PARAM: param,
ATTR_PROXY: proxy,
ATTR_INTERFACE: interface,
}
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
def set_var_value(hass, entity_id, value):
@bind_hass
def set_variable_value(hass, entity_id, value):
"""Change value of a Homematic system variable."""
data = {
ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: value,
}
hass.services.call(DOMAIN, SERVICE_SET_VAR_VALUE, data)
hass.services.call(DOMAIN, SERVICE_SET_VARIABLE_VALUE, data)
def set_dev_value(hass, address, channel, param, value, proxy=None):
"""Call setValue XML-RPC method of supplied proxy."""
@bind_hass
def set_device_value(hass, address, channel, param, value, interface=None):
"""Call setValue XML-RPC method of supplied interface."""
data = {
ATTR_ADDRESS: address,
ATTR_CHANNEL: channel,
ATTR_PARAM: param,
ATTR_VALUE: value,
ATTR_PROXY: proxy,
ATTR_INTERFACE: interface,
}
hass.services.call(DOMAIN, SERVICE_SET_DEV_VALUE, data)
hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data)
@bind_hass
def set_install_mode(hass, interface, mode=None, time=None, address=None):
"""Call setInstallMode XML-RPC method of supplied inteface."""
data = {
key: value for key, value in (
(ATTR_INTERFACE, interface),
(ATTR_MODE, mode),
(ATTR_TIME, time),
(ATTR_ADDRESS, address)
) if value
}
hass.services.call(DOMAIN, SERVICE_SET_INSTALL_MODE, data)
@bind_hass
def reconnect(hass):
"""Reconnect to CCU/Homegear."""
hass.services.call(DOMAIN, SERVICE_RECONNECT, {})
@@ -250,31 +277,32 @@ def setup(hass, config):
"""Set up the Homematic component."""
from pyhomematic import HMConnection
hass.data[DATA_DEVINIT] = {}
conf = config[DOMAIN]
hass.data[DATA_CONF] = remotes = {}
hass.data[DATA_STORE] = set()
# Create hosts-dictionary for pyhomematic
remotes = {}
hosts = {}
for rname, rconfig in config[DOMAIN][CONF_HOSTS].items():
server = rconfig.get(CONF_IP)
for rname, rconfig in conf[CONF_INTERFACES].items():
remotes[rname] = {
'ip': socket.gethostbyname(rconfig.get(CONF_HOST)),
'port': rconfig.get(CONF_PORT),
'path': rconfig.get(CONF_PATH),
'resolvenames': rconfig.get(CONF_RESOLVENAMES),
'username': rconfig.get(CONF_USERNAME),
'password': rconfig.get(CONF_PASSWORD),
'callbackip': rconfig.get(CONF_CALLBACK_IP),
'callbackport': rconfig.get(CONF_CALLBACK_PORT),
'connect': True,
}
remotes[rname] = {}
remotes[rname][CONF_IP] = server
remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT)
remotes[rname][CONF_PATH] = rconfig.get(CONF_PATH)
remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES)
remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME)
remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD)
remotes[rname]['callbackip'] = rconfig.get(CONF_CALLBACK_IP)
remotes[rname]['callbackport'] = rconfig.get(CONF_CALLBACK_PORT)
if server not in hosts or rconfig.get(CONF_PRIMARY):
hosts[server] = {
CONF_VARIABLES: rconfig.get(CONF_VARIABLES),
CONF_NAME: rname,
}
hass.data[DATA_DEVINIT][rname] = rconfig.get(CONF_DEVICES)
for sname, sconfig in conf[CONF_HOSTS].items():
remotes[sname] = {
'ip': socket.gethostbyname(sconfig.get(CONF_HOST)),
'port': DEFAULT_PORT,
'username': sconfig.get(CONF_USERNAME),
'password': sconfig.get(CONF_PASSWORD),
'connect': False,
}
# Create server thread
bound_system_callback = partial(_system_callback_handler, hass, config)
@@ -295,9 +323,8 @@ def setup(hass, config):
# Init homematic hubs
entity_hubs = []
for _, hub_data in hosts.items():
entity_hubs.append(HMHub(
hass, homematic, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
for hub_name in conf[CONF_HOSTS].keys():
entity_hubs.append(HMHub(hass, homematic, hub_name))
# Register HomeMatic services
descriptions = load_yaml_config_file(
@@ -331,8 +358,7 @@ def setup(hass, config):
hass.services.register(
DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey,
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
schema=SCHEMA_SERVICE_VIRTUALKEY)
descriptions[SERVICE_VIRTUALKEY], schema=SCHEMA_SERVICE_VIRTUALKEY)
def _service_handle_value(service):
"""Service to call setValue method for HomeMatic system variable."""
@@ -354,9 +380,9 @@ def setup(hass, config):
hub.hm_set_variable(name, value)
hass.services.register(
DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value,
descriptions[DOMAIN][SERVICE_SET_VAR_VALUE],
schema=SCHEMA_SERVICE_SET_VAR_VALUE)
DOMAIN, SERVICE_SET_VARIABLE_VALUE, _service_handle_value,
descriptions[SERVICE_SET_VARIABLE_VALUE],
schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE)
def _service_handle_reconnect(service):
"""Service to reconnect all HomeMatic hubs."""
@@ -364,8 +390,7 @@ def setup(hass, config):
hass.services.register(
DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect,
descriptions[DOMAIN][SERVICE_RECONNECT],
schema=SCHEMA_SERVICE_RECONNECT)
descriptions[SERVICE_RECONNECT], schema=SCHEMA_SERVICE_RECONNECT)
def _service_handle_device(service):
"""Service to call setValue method for HomeMatic devices."""
@@ -383,9 +408,23 @@ def setup(hass, config):
hmdevice.setValue(param, value, channel)
hass.services.register(
DOMAIN, SERVICE_SET_DEV_VALUE, _service_handle_device,
descriptions[DOMAIN][SERVICE_SET_DEV_VALUE],
schema=SCHEMA_SERVICE_SET_DEV_VALUE)
DOMAIN, SERVICE_SET_DEVICE_VALUE, _service_handle_device,
descriptions[SERVICE_SET_DEVICE_VALUE],
schema=SCHEMA_SERVICE_SET_DEVICE_VALUE)
def _service_handle_install_mode(service):
"""Service to set interface into install mode."""
interface = service.data.get(ATTR_INTERFACE)
mode = service.data.get(ATTR_MODE)
time = service.data.get(ATTR_TIME)
address = service.data.get(ATTR_ADDRESS)
homematic.setInstallMode(interface, t=time, mode=mode, address=address)
hass.services.register(
DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode,
descriptions[SERVICE_SET_INSTALL_MODE],
schema=SCHEMA_SERVICE_SET_INSTALL_MODE)
return True
@@ -395,10 +434,10 @@ def _system_callback_handler(hass, config, src, *args):
# New devices available at hub
if src == 'newDevices':
(interface_id, dev_descriptions) = args
proxy = interface_id.split('-')[-1]
interface = interface_id.split('-')[-1]
# Device support active?
if not hass.data[DATA_DEVINIT][proxy]:
if not hass.data[DATA_CONF][interface]['connect']:
return
addresses = []
@@ -410,9 +449,9 @@ def _system_callback_handler(hass, config, src, *args):
# Register EVENTS
# Search all devices with an EVENTNODE that includes data
bound_event_callback = partial(_hm_event_handler, hass, proxy)
bound_event_callback = partial(_hm_event_handler, hass, interface)
for dev in addresses:
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev)
hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(dev)
if hmdevice.EVENTNODE:
hmdevice.setEventCallback(
@@ -429,7 +468,7 @@ def _system_callback_handler(hass, config, src, *args):
('climate', DISCOVER_CLIMATE)):
# Get all devices of a specific type
found_devices = _get_devices(
hass, discovery_type, addresses, proxy)
hass, discovery_type, addresses, interface)
# When devices of this type are found
# they are setup in HASS and an discovery event is fired
@@ -448,12 +487,12 @@ def _system_callback_handler(hass, config, src, *args):
})
def _get_devices(hass, discovery_type, keys, proxy):
def _get_devices(hass, discovery_type, keys, interface):
"""Get the HomeMatic devices for given discovery_type."""
device_arr = []
for key in keys:
device = hass.data[DATA_HOMEMATIC].devices[proxy][key]
device = hass.data[DATA_HOMEMATIC].devices[interface][key]
class_name = device.__class__.__name__
metadata = {}
@@ -485,7 +524,7 @@ def _get_devices(hass, discovery_type, keys, proxy):
device_dict = {
CONF_PLATFORM: "homematic",
ATTR_ADDRESS: key,
ATTR_PROXY: proxy,
ATTR_INTERFACE: interface,
ATTR_NAME: name,
ATTR_CHANNEL: channel
}
@@ -521,12 +560,12 @@ def _create_ha_name(name, channel, param, count):
return "{} {} {}".format(name, channel, param)
def _hm_event_handler(hass, proxy, device, caller, attribute, value):
def _hm_event_handler(hass, interface, device, caller, attribute, value):
"""Handle all pyhomematic device events."""
try:
channel = int(device.split(":")[1])
address = device.split(":")[0]
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(address)
except (TypeError, ValueError):
_LOGGER.error("Event handling channel convert error!")
return
@@ -561,14 +600,14 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
def _device_from_servicecall(hass, service):
"""Extract HomeMatic device from service call."""
address = service.data.get(ATTR_ADDRESS)
proxy = service.data.get(ATTR_PROXY)
interface = service.data.get(ATTR_INTERFACE)
if address == 'BIDCOS-RF':
address = 'BidCoS-RF'
if proxy:
return hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
if interface:
return hass.data[DATA_HOMEMATIC].devices[interface].get(address)
for _, devices in hass.data[DATA_HOMEMATIC].devices.items():
for devices in hass.data[DATA_HOMEMATIC].devices.values():
if address in devices:
return devices[address]
@@ -576,25 +615,23 @@ def _device_from_servicecall(hass, service):
class HMHub(Entity):
"""The HomeMatic hub. (CCU2/HomeGear)."""
def __init__(self, hass, homematic, name, use_variables):
def __init__(self, hass, homematic, name):
"""Initialize HomeMatic hub."""
self.hass = hass
self.entity_id = "{}.{}".format(DOMAIN, name.lower())
self._homematic = homematic
self._variables = {}
self._name = name
self._state = STATE_UNKNOWN
self._use_variables = use_variables
self._state = None
# Load data
track_time_interval(
self.hass, self._update_hub, SCAN_INTERVAL_HUB)
self.hass.helpers.event.track_time_interval(
self._update_hub, SCAN_INTERVAL_HUB)
self.hass.add_job(self._update_hub, None)
if self._use_variables:
track_time_interval(
self.hass, self._update_variables, SCAN_INTERVAL_VARIABLES)
self.hass.add_job(self._update_variables, None)
self.hass.helpers.event.track_time_interval(
self._update_variables, SCAN_INTERVAL_VARIABLES)
self.hass.add_job(self._update_variables, None)
@property
def name(self):
@@ -672,7 +709,7 @@ class HMDevice(Entity):
"""Initialize a generic HomeMatic device."""
self._name = config.get(ATTR_NAME)
self._address = config.get(ATTR_ADDRESS)
self._proxy = config.get(ATTR_PROXY)
self._interface = config.get(ATTR_INTERFACE)
self._channel = config.get(ATTR_CHANNEL)
self._state = config.get(ATTR_PARAM)
self._data = {}
@@ -700,11 +737,6 @@ class HMDevice(Entity):
"""Return the name of the device."""
return self._name
@property
def assumed_state(self):
"""Return true if unable to access real state of the device."""
return not self._available
@property
def available(self):
"""Return true if device is available."""
@@ -728,7 +760,7 @@ class HMDevice(Entity):
# Static attributes
attr['id'] = self._hmdevice.ADDRESS
attr['proxy'] = self._proxy
attr['interface'] = self._interface
return attr
@@ -739,7 +771,8 @@ class HMDevice(Entity):
# Initialize
self._homematic = self.hass.data[DATA_HOMEMATIC]
self._hmdevice = self._homematic.devices[self._proxy][self._address]
self._hmdevice = \
self._homematic.devices[self._interface][self._address]
self._connected = True
try:
@@ -0,0 +1,68 @@
# Describes the format for available component services
virtualkey:
description: Press a virtual key from CCU/Homegear or simulate keypress.
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote.
example: BidCoS-RF
channel:
description: Channel for calling a keypress.
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
example: PRESS_LONG
interface:
description: (Optional) for set a interface value.
example: Interfaces name from config
set_variable_value:
description: Set the name of a node.
fields:
entity_id:
description: Name(s) of homematic central to set value.
example: 'homematic.ccu2'
name:
description: Name of the variable to set.
example: 'testvariable'
value:
description: New value
example: 1
set_device_value:
description: Set a device property on RPC XML interface.
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote
example: BidCoS-RF
channel:
description: Channel for calling a keypress
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
example: PRESS_LONG
interface:
description: (Optional) for set a interface value
example: Interfaces name from config
value:
description: New value
example: 1
reconnect:
description: Reconnect to all Homematic Hubs.
set_install_mode:
description: Set a RPC XML interface into installation mode.
fields:
interface:
description: Select the given interface into install mode
example: Interfaces name from config
mode:
description: (Default 1) 1= Normal mode / 2= Remove exists old links
example: 1
time:
description: (Default 60) Time in seconds to run in install mode
example: 1
address:
description: (Optional) Address of homematic device or BidCoS-RF to learn
example: LEQ3948571
+244
View File
@@ -0,0 +1,244 @@
"""
This component provides basic support for the Philips Hue system.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hue/
"""
import json
import logging
import os
import socket
import voluptuous as vol
from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.config import load_yaml_config_file
from homeassistant.const import CONF_FILENAME, CONF_HOST
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
REQUIREMENTS = ['phue==1.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "hue"
SERVICE_HUE_SCENE = "hue_activate_scene"
CONF_BRIDGES = "bridges"
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
DEFAULT_ALLOW_UNREACHABLE = False
PHUE_CONFIG_FILE = 'phue.conf'
CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue"
DEFAULT_ALLOW_IN_EMULATED_HUE = True
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
DEFAULT_ALLOW_HUE_GROUPS = True
BRIDGE_CONFIG_SCHEMA = vol.Schema([{
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE,
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
vol.Optional(CONF_ALLOW_IN_EMULATED_HUE,
default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean,
vol.Optional(CONF_ALLOW_HUE_GROUPS,
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
}])
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_BRIDGES, default=[]): BRIDGE_CONFIG_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
})
CONFIG_INSTRUCTIONS = """
Press the button on the bridge to register Philips Hue with Home Assistant.
![Location of button on bridge](/static/images/config_philips_hue.jpg)
"""
def setup(hass, config):
"""Set up the Hue platform."""
config = config.get(DOMAIN)
if config is None:
config = {}
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
discovery.listen(
hass,
SERVICE_HUE,
lambda service, discovery_info:
bridge_discovered(hass, service, discovery_info))
bridges = config.get(CONF_BRIDGES, [])
for bridge in bridges:
filename = bridge.get(CONF_FILENAME)
allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE)
allow_in_emulated_hue = bridge.get(CONF_ALLOW_IN_EMULATED_HUE)
allow_hue_groups = bridge.get(CONF_ALLOW_HUE_GROUPS)
host = bridge.get(CONF_HOST)
if host is None:
host = _find_host_from_config(hass, filename)
if host is None:
_LOGGER.error("No host found in configuration")
return False
setup_bridge(host, hass, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
return True
def bridge_discovered(hass, service, discovery_info):
"""Dispatcher for Hue discovery events."""
if "HASS Bridge" in discovery_info.get('name', ''):
return
host = discovery_info.get('host')
serial = discovery_info.get('serial')
filename = 'phue-{}.conf'.format(serial)
setup_bridge(host, hass, filename)
def setup_bridge(host, hass, filename=None, allow_unreachable=False,
allow_in_emulated_hue=True, allow_hue_groups=True):
"""Set up a given Hue bridge."""
# Only register a device once
if socket.gethostbyname(host) in hass.data[DOMAIN]:
return
bridge = HueBridge(host, hass, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
bridge.setup()
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
"""Attempt to detect host based on existing configuration."""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
try:
with open(path) as inp:
return next(iter(json.load(inp).keys()))
except (ValueError, AttributeError, StopIteration):
# ValueError if can't parse as JSON
# AttributeError if JSON value is not a dict
# StopIteration if no keys
return None
class HueBridge(object):
"""Manages a single Hue bridge."""
def __init__(self, host, hass, filename, allow_unreachable=False,
allow_in_emulated_hue=True, allow_hue_groups=True):
"""Initialize the system."""
self.host = host
self.hass = hass
self.filename = filename
self.allow_unreachable = allow_unreachable
self.allow_in_emulated_hue = allow_in_emulated_hue
self.allow_hue_groups = allow_hue_groups
self.bridge = None
self.configured = False
self.config_request_id = None
hass.data[DOMAIN][socket.gethostbyname(host)] = self
def setup(self):
"""Set up a phue bridge based on host parameter."""
import phue
try:
self.bridge = phue.Bridge(
self.host,
config_file_path=self.hass.config.path(self.filename))
except ConnectionRefusedError: # Wrong host was given
_LOGGER.error("Error connecting to the Hue bridge at %s",
self.host)
return
except phue.PhueRegistrationException:
_LOGGER.warning("Connected to Hue at %s but not registered.",
self.host)
self.request_configuration()
return
# If we came here and configuring this host, mark as done
if self.config_request_id:
request_id = self.config_request_id
self.config_request_id = None
configurator = self.hass.components.configurator
configurator.request_done(request_id)
self.configured = True
discovery.load_platform(
self.hass, 'light', DOMAIN,
{'bridge_id': socket.gethostbyname(self.host)})
# create a service for calling run_scene directly on the bridge,
# used to simplify automation rules.
def hue_activate_scene(call):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
self.bridge.run_scene(group_name, scene_name)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
self.hass.services.register(
DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
descriptions.get(SERVICE_HUE_SCENE),
schema=SCENE_SCHEMA)
def request_configuration(self):
"""Request configuration steps from the user."""
configurator = self.hass.components.configurator
# We got an error if this method is called while we are configuring
if self.config_request_id:
configurator.notify_errors(
self.config_request_id,
"Failed to register, please try again.")
return
self.config_request_id = configurator.request_config(
"Philips Hue",
lambda data: self.setup(),
description=CONFIG_INSTRUCTIONS,
entity_picture="/static/images/logo_philips_hue.png",
submit_caption="I have pressed the button"
)
def get_api(self):
"""Return the full api dictionary from phue."""
return self.bridge.get_api()
def set_light(self, light_id, command):
"""Adjust properties of one or more lights. See phue for details."""
return self.bridge.set_light(light_id, command)
def set_group(self, light_id, command):
"""Change light settings for a group. See phue for detail."""
return self.bridge.set_group(light_id, command)
+1 -1
View File
@@ -264,7 +264,7 @@ class iOSIdentifyDeviceView(HomeAssistantView):
# return self.json_message(humanize_error(request.json, ex),
# HTTP_BAD_REQUEST)
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now()
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat()
name = data.get(ATTR_DEVICE_ID)
+62 -2
View File
@@ -4,6 +4,7 @@ Support the ISY-994 controllers.
For configuration details please visit the documentation for this component at
https://home-assistant.io/components/isy994/
"""
import asyncio
from collections import namedtuple
import logging
from urllib.parse import urlparse
@@ -17,7 +18,7 @@ from homeassistant.helpers import discovery, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, Dict # noqa
REQUIREMENTS = ['PyISY==1.0.8']
REQUIREMENTS = ['PyISY==1.1.0']
_LOGGER = logging.getLogger(__name__)
@@ -91,6 +92,34 @@ def filter_nodes(nodes: list, units: list=None, states: list=None) -> list:
return filtered_nodes
def _is_node_a_sensor(node, path: str, sensor_identifier: str) -> bool:
"""Determine if the given node is a sensor."""
if not isinstance(node, PYISY.Nodes.Node):
return False
if sensor_identifier in path or sensor_identifier in node.name:
return True
# This method is most reliable but only works on 5.x firmware
try:
if node.node_def_id == 'BinaryAlarm':
return True
except AttributeError:
pass
# This method works on all firmwares, but only for Insteon devices
try:
device_type = node.type
except AttributeError:
# Node has no type; most likely not an Insteon device
pass
else:
split_type = device_type.split('.')
return split_type[0] == '16' # 16 represents Insteon binary sensors
return False
def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
"""Categorize the ISY994 nodes."""
global SENSOR_NODES
@@ -106,7 +135,7 @@ def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
hidden = hidden_identifier in path or hidden_identifier in node.name
if hidden:
node.name += hidden_identifier
if sensor_identifier in path or sensor_identifier in node.name:
if _is_node_a_sensor(node, path, sensor_identifier):
SENSOR_NODES.append(node)
elif isinstance(node, PYISY.Nodes.Node):
NODES.append(node)
@@ -227,15 +256,31 @@ class ISYDevice(Entity):
def __init__(self, node) -> None:
"""Initialize the insteon device."""
self._node = node
self._change_handler = None
self._control_handler = None
@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Subscribe to the node change events."""
self._change_handler = self._node.status.subscribe(
'changed', self.on_update)
if hasattr(self._node, 'controlEvents'):
self._control_handler = self._node.controlEvents.subscribe(
self.on_control)
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node."""
self.schedule_update_ha_state()
def on_control(self, event: object) -> None:
"""Handle a control event from the ISY994 Node."""
self.hass.bus.fire('isy994_control', {
'entity_id': self.entity_id,
'control': event
})
@property
def domain(self) -> str:
"""Get the domain of the device."""
@@ -270,6 +315,21 @@ class ISYDevice(Entity):
# pylint: disable=protected-access
return self._node.status._val
def is_unknown(self) -> bool:
"""Get whether or not the value of this Entity's node is unknown.
PyISY reports unknown values as -inf
"""
return self.value == -1 * float('inf')
@property
def state(self):
"""Return the state of the ISY device."""
if self.is_unknown():
return None
else:
return super().state
@property
def device_state_attributes(self) -> Dict:
"""Get the state attributes for the device."""
+48 -21
View File
@@ -21,7 +21,7 @@ REQUIREMENTS = ['evdev==0.6.1']
_LOGGER = logging.getLogger(__name__)
DEVICE_DESCRIPTOR = 'device_descriptor'
DEVICE_ID_GROUP = 'Device descriptor or name'
DEVICE_ID_GROUP = 'Device description'
DEVICE_NAME = 'device_name'
DOMAIN = 'keyboard_remote'
@@ -36,12 +36,13 @@ KEYBOARD_REMOTE_DISCONNECTED = 'keyboard_remote_disconnected'
TYPE = 'type'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string,
vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string,
vol.Optional(TYPE, default='key_up'):
vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold')),
}),
DOMAIN:
vol.All(cv.ensure_list, [vol.Schema({
vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string,
vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string,
vol.Optional(TYPE, default='key_up'):
vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold'))
})])
}, extra=vol.ALLOW_EXTRA)
@@ -49,11 +50,6 @@ def setup(hass, config):
"""Set up the keyboard_remote."""
config = config.get(DOMAIN)
if not config.get(DEVICE_DESCRIPTOR) and\
not config.get(DEVICE_NAME):
_LOGGER.error("No device_descriptor or device_name found")
return
keyboard_remote = KeyboardRemote(
hass,
config
@@ -63,7 +59,7 @@ def setup(hass, config):
keyboard_remote.run()
def _stop_keyboard_remote(_event):
keyboard_remote.stopped.set()
keyboard_remote.stop()
hass.bus.listen_once(
EVENT_HOMEASSISTANT_START,
@@ -77,19 +73,21 @@ def setup(hass, config):
return True
class KeyboardRemote(threading.Thread):
class KeyboardRemoteThread(threading.Thread):
"""This interfaces with the inputdevice using evdev."""
def __init__(self, hass, config):
"""Construct a KeyboardRemote interface object."""
from evdev import InputDevice, list_devices
def __init__(self, hass, device_name, device_descriptor, key_value):
"""Construct a thread listening for events on one device."""
self.hass = hass
self.device_name = device_name
self.device_descriptor = device_descriptor
self.key_value = key_value
self.device_descriptor = config.get(DEVICE_DESCRIPTOR)
self.device_name = config.get(DEVICE_NAME)
if self.device_descriptor:
self.device_id = self.device_descriptor
else:
self.device_id = self.device_name
self.dev = self._get_keyboard_device()
if self.dev is not None:
_LOGGER.debug("Keyboard connected, %s", self.device_id)
@@ -103,6 +101,7 @@ class KeyboardRemote(threading.Thread):
id_folder = '/dev/input/by-id/'
if os.path.isdir(id_folder):
from evdev import InputDevice, list_devices
device_names = [InputDevice(file_name).name
for file_name in list_devices()]
_LOGGER.debug(
@@ -116,7 +115,6 @@ class KeyboardRemote(threading.Thread):
threading.Thread.__init__(self)
self.stopped = threading.Event()
self.hass = hass
self.key_value = KEY_VALUE.get(config.get(TYPE, 'key_up'))
def _get_keyboard_device(self):
"""Get the keyboard device."""
@@ -145,7 +143,7 @@ class KeyboardRemote(threading.Thread):
while not self.stopped.isSet():
# Sleeps to ease load on processor
time.sleep(.1)
time.sleep(.05)
if self.dev is None:
self.dev = self._get_keyboard_device()
@@ -178,3 +176,32 @@ class KeyboardRemote(threading.Thread):
KEYBOARD_REMOTE_COMMAND_RECEIVED,
{KEY_CODE: event.code}
)
class KeyboardRemote(object):
"""Sets up one thread per device."""
def __init__(self, hass, config):
"""Construct a KeyboardRemote interface object."""
self.threads = []
for dev_block in config:
device_descriptor = dev_block.get(DEVICE_DESCRIPTOR)
device_name = dev_block.get(DEVICE_NAME)
key_value = KEY_VALUE.get(dev_block.get(TYPE, 'key_up'))
if device_descriptor is not None\
or device_name is not None:
thread = KeyboardRemoteThread(hass, device_name,
device_descriptor,
key_value)
self.threads.append(thread)
def run(self):
"""Run all event listener threads."""
for thread in self.threads:
thread.start()
def stop(self):
"""Stop all event listener threads."""
for thread in self.threads:
thread.stopped.set()
+117
View File
@@ -0,0 +1,117 @@
"""
Support for ADS light sources.
For more details about this platform, please refer to the documentation.
https://home-assistant.io/components/light.ads/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.light import Light, ATTR_BRIGHTNESS, \
SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR, \
CONF_ADS_VAR_BRIGHTNESS
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['ads']
DEFAULT_NAME = 'ADS Light'
CONF_ADSVAR_BRIGHTNESS = 'adsvar_brightness'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the light platform for ADS."""
ads_hub = hass.data.get(DATA_ADS)
ads_var_enable = config.get(CONF_ADS_VAR)
ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS)
name = config.get(CONF_NAME)
add_devices([AdsLight(ads_hub, ads_var_enable, ads_var_brightness,
name)], True)
class AdsLight(Light):
"""Representation of ADS light."""
def __init__(self, ads_hub, ads_var_enable, ads_var_brightness, name):
"""Initialize AdsLight entity."""
self._ads_hub = ads_hub
self._on_state = False
self._brightness = None
self._name = name
self.ads_var_enable = ads_var_enable
self.ads_var_brightness = ads_var_brightness
@asyncio.coroutine
def async_added_to_hass(self):
"""Register device notification."""
def update_on_state(name, value):
"""Handle device notifications for state."""
_LOGGER.debug('Variable %s changed its value to %d', name, value)
self._on_state = value
self.schedule_update_ha_state()
def update_brightness(name, value):
"""Handle device notification for brightness."""
_LOGGER.debug('Variable %s changed its value to %d', name, value)
self._brightness = value
self.schedule_update_ha_state()
self.hass.async_add_job(
self._ads_hub.add_device_notification,
self.ads_var_enable, self._ads_hub.PLCTYPE_BOOL, update_on_state
)
self.hass.async_add_job(
self._ads_hub.add_device_notification,
self.ads_var_brightness, self._ads_hub.PLCTYPE_INT,
update_brightness
)
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def brightness(self):
"""Return the brightness of the light (0..255)."""
return self._brightness
@property
def is_on(self):
"""Return if light is on."""
return self._on_state
@property
def should_poll(self):
"""Return False because entity pushes its state to HA."""
return False
@property
def supported_features(self):
"""Flag supported features."""
if self.ads_var_brightness is not None:
return SUPPORT_BRIGHTNESS
def turn_on(self, **kwargs):
"""Turn the light on or set a specific dimmer value."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
self._ads_hub.write_by_name(self.ads_var_enable, True,
self._ads_hub.PLCTYPE_BOOL)
if self.ads_var_brightness is not None and brightness is not None:
self._ads_hub.write_by_name(self.ads_var_brightness, brightness,
self._ads_hub.PLCTYPE_UINT)
def turn_off(self, **kwargs):
"""Turn the light off."""
self._ads_hub.write_by_name(self.ads_var_enable, False,
self._ads_hub.PLCTYPE_BOOL)
+144 -214
View File
@@ -1,19 +1,21 @@
"""
Support for Hue lights.
This component provides light support for the Philips Hue system.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hue/
"""
import json
import logging
import os
import random
import socket
from datetime import timedelta
import logging
import random
import re
import socket
import voluptuous as vol
import homeassistant.components.hue as hue
import homeassistant.util as util
from homeassistant.util import yaml
import homeassistant.util.color as color_util
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
@@ -21,30 +23,21 @@ from homeassistant.components.light import (
FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME)
from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE_HIDDEN
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['phue==1.0']
DEPENDENCIES = ['hue']
# Track previously setup bridges
_CONFIGURED_BRIDGES = {}
# Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
DEFAULT_ALLOW_UNREACHABLE = False
DOMAIN = "light"
SERVICE_HUE_SCENE = "hue_activate_scene"
DATA_KEY = 'hue_lights'
DATA_LIGHTS = 'lights'
DATA_LIGHTGROUPS = 'lightgroups'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
PHUE_CONFIG_FILE = 'phue.conf'
SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION)
SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
@@ -60,10 +53,14 @@ SUPPORT_HUE = {
'Color temperature light': SUPPORT_HUE_COLOR_TEMP
}
CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue"
DEFAULT_ALLOW_IN_EMULATED_HUE = True
ATTR_IS_HUE_GROUP = 'is_hue_group'
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
# Legacy configuration, will be removed in 0.60
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
DEFAULT_ALLOW_UNREACHABLE = False
CONF_ALLOW_IN_EMULATED_HUE = 'allow_in_emulated_hue'
DEFAULT_ALLOW_IN_EMULATED_HUE = True
CONF_ALLOW_HUE_GROUPS = 'allow_hue_groups'
DEFAULT_ALLOW_HUE_GROUPS = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -75,236 +72,168 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
})
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
})
MIGRATION_ID = 'light_hue_config_migration'
MIGRATION_TITLE = 'Philips Hue Configuration Migration'
MIGRATION_INSTRUCTIONS = """
Configuration for the Philips Hue component has changed; action required.
ATTR_IS_HUE_GROUP = "is_hue_group"
You have configured at least one bridge:
CONFIG_INSTRUCTIONS = """
Press the button on the bridge to register Philips Hue with Home Assistant.
hue:
{config}
![Location of button on bridge](/static/images/config_philips_hue.jpg)
This configuration is deprecated, please check the
[Hue component](https://home-assistant.io/components/hue/) page for more
information.
"""
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
"""Attempt to detect host based on existing configuration."""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
try:
with open(path) as inp:
return next(json.loads(''.join(inp)).keys().__iter__())
except (ValueError, AttributeError, StopIteration):
# ValueError if can't parse as JSON
# AttributeError if JSON value is not a dict
# StopIteration if no keys
return None
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Hue lights."""
# Default needed in case of discovery
filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE)
allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE,
DEFAULT_ALLOW_UNREACHABLE)
allow_in_emulated_hue = config.get(CONF_ALLOW_IN_EMULATED_HUE,
DEFAULT_ALLOW_IN_EMULATED_HUE)
allow_hue_groups = config.get(CONF_ALLOW_HUE_GROUPS)
if discovery_info is not None:
if "HASS Bridge" in discovery_info.get('name', ''):
_LOGGER.info("Emulated hue found, will not add")
return False
host = discovery_info.get('host')
else:
host = config.get(CONF_HOST, None)
if host is None:
host = _find_host_from_config(hass, filename)
if host is None:
_LOGGER.error("No host found in configuration")
return False
# Only act if we are not already configuring this host
if host in _CONFIGURING or \
socket.gethostbyname(host) in _CONFIGURED_BRIDGES:
if discovery_info is None or 'bridge_id' not in discovery_info:
return
setup_bridge(host, hass, add_devices, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
setup_data(hass)
if config is not None and len(config) > 0:
# Legacy configuration, will be removed in 0.60
config_str = yaml.dump([config])
# Indent so it renders in a fixed-width font
config_str = re.sub('(?m)^', ' ', config_str)
hass.components.persistent_notification.async_create(
MIGRATION_INSTRUCTIONS.format(config=config_str),
title=MIGRATION_TITLE,
notification_id=MIGRATION_ID)
bridge_id = discovery_info['bridge_id']
bridge = hass.data[hue.DOMAIN][bridge_id]
unthrottled_update_lights(hass, bridge, add_devices)
def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups):
"""Set up a phue bridge based on host parameter."""
def setup_data(hass):
"""Initialize internal data. Useful from tests."""
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {DATA_LIGHTS: {}, DATA_LIGHTGROUPS: {}}
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights(hass, bridge, add_devices):
"""Update the Hue light objects with latest info from the bridge."""
return unthrottled_update_lights(hass, bridge, add_devices)
def unthrottled_update_lights(hass, bridge, add_devices):
"""Internal version of update_lights."""
import phue
if not bridge.configured:
return
try:
bridge = phue.Bridge(
host,
config_file_path=hass.config.path(filename))
except ConnectionRefusedError: # Wrong host was given
_LOGGER.error("Error connecting to the Hue bridge at %s", host)
api = bridge.get_api()
except phue.PhueRequestTimeout:
_LOGGER.warning('Timeout trying to reach the bridge')
return
except ConnectionRefusedError:
_LOGGER.error('The bridge refused the connection')
return
except socket.error:
# socket.error when we cannot reach Hue
_LOGGER.exception('Cannot reach the bridge')
return
except phue.PhueRegistrationException:
_LOGGER.warning("Connected to Hue at %s but not registered.", host)
bridge_type = get_bridge_type(api)
request_configuration(host, hass, add_devices, filename,
allow_unreachable, allow_in_emulated_hue,
allow_hue_groups)
new_lights = process_lights(
hass, api, bridge, bridge_type,
lambda **kw: update_lights(hass, bridge, add_devices, **kw))
if bridge.allow_hue_groups:
new_lightgroups = process_groups(
hass, api, bridge, bridge_type,
lambda **kw: update_lights(hass, bridge, add_devices, **kw))
new_lights.extend(new_lightgroups)
return
if new_lights:
add_devices(new_lights)
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = hass.components.configurator
configurator.request_done(request_id)
lights = {}
lightgroups = {}
skip_groups = not allow_hue_groups
def get_bridge_type(api):
"""Return the bridge type."""
api_name = api.get('config').get('name')
if api_name in ('RaspBee-GW', 'deCONZ-GW'):
return 'deconz'
else:
return 'hue'
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights():
"""Update the Hue light objects with latest info from the bridge."""
nonlocal skip_groups
try:
api = bridge.get_api()
except phue.PhueRequestTimeout:
_LOGGER.warning("Timeout trying to reach the bridge")
return
except ConnectionRefusedError:
_LOGGER.error("The bridge refused the connection")
return
except socket.error:
# socket.error when we cannot reach Hue
_LOGGER.exception("Cannot reach the bridge")
return
def process_lights(hass, api, bridge, bridge_type, update_lights_cb):
"""Set up HueLight objects for all lights."""
api_lights = api.get('lights')
api_lights = api.get('lights')
if not isinstance(api_lights, dict):
_LOGGER.error('Got unexpected result from Hue API')
return []
if not isinstance(api_lights, dict):
_LOGGER.error("Got unexpected result from Hue API")
return
new_lights = []
if skip_groups:
api_groups = {}
lights = hass.data[DATA_KEY][DATA_LIGHTS]
for light_id, info in api_lights.items():
if light_id not in lights:
lights[light_id] = HueLight(
int(light_id), info, bridge,
update_lights_cb,
bridge_type, bridge.allow_unreachable,
bridge.allow_in_emulated_hue)
new_lights.append(lights[light_id])
else:
api_groups = api.get('groups')
lights[light_id].info = info
lights[light_id].schedule_update_ha_state()
if not isinstance(api_groups, dict):
_LOGGER.error("Got unexpected result from Hue API")
return
return new_lights
new_lights = []
api_name = api.get('config').get('name')
if api_name in ('RaspBee-GW', 'deCONZ-GW'):
bridge_type = 'deconz'
def process_groups(hass, api, bridge, bridge_type, update_lights_cb):
"""Set up HueLight objects for all groups."""
api_groups = api.get('groups')
if not isinstance(api_groups, dict):
_LOGGER.error('Got unexpected result from Hue API')
return []
new_lights = []
groups = hass.data[DATA_KEY][DATA_LIGHTGROUPS]
for lightgroup_id, info in api_groups.items():
if 'state' not in info:
_LOGGER.warning('Group info does not contain state. '
'Please update your hub.')
return []
if lightgroup_id not in groups:
groups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge,
update_lights_cb,
bridge_type, bridge.allow_unreachable,
bridge.allow_in_emulated_hue, True)
new_lights.append(groups[lightgroup_id])
else:
bridge_type = 'hue'
groups[lightgroup_id].info = info
groups[lightgroup_id].schedule_update_ha_state()
for light_id, info in api_lights.items():
if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights,
bridge_type, allow_unreachable,
allow_in_emulated_hue)
new_lights.append(lights[light_id])
else:
lights[light_id].info = info
lights[light_id].schedule_update_ha_state()
for lightgroup_id, info in api_groups.items():
if 'state' not in info:
_LOGGER.warning("Group info does not contain state. "
"Please update your hub.")
skip_groups = True
break
if lightgroup_id not in lightgroups:
lightgroups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge, update_lights,
bridge_type, allow_unreachable, allow_in_emulated_hue,
True)
new_lights.append(lightgroups[lightgroup_id])
else:
lightgroups[lightgroup_id].info = info
lightgroups[lightgroup_id].schedule_update_ha_state()
if new_lights:
add_devices(new_lights)
_CONFIGURED_BRIDGES[socket.gethostbyname(host)] = True
# create a service for calling run_scene directly on the bridge,
# used to simplify automation rules.
def hue_activate_scene(call):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
bridge.run_scene(group_name, scene_name)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
descriptions.get(SERVICE_HUE_SCENE),
schema=SCENE_SCHEMA)
update_lights()
def request_configuration(host, hass, add_devices, filename,
allow_unreachable, allow_in_emulated_hue,
allow_hue_groups):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], "Failed to register, please try again.")
return
# pylint: disable=unused-argument
def hue_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
setup_bridge(host, hass, add_devices, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
_CONFIGURING[host] = configurator.request_config(
"Philips Hue", hue_configuration_callback,
description=CONFIG_INSTRUCTIONS,
entity_picture="/static/images/logo_philips_hue.png",
submit_caption="I have pressed the button"
)
return new_lights
class HueLight(Light):
"""Representation of a Hue light."""
def __init__(self, light_id, info, bridge, update_lights,
def __init__(self, light_id, info, bridge, update_lights_cb,
bridge_type, allow_unreachable, allow_in_emulated_hue,
is_group=False):
"""Initialize the light."""
self.light_id = light_id
self.info = info
self.bridge = bridge
self.update_lights = update_lights
self.update_lights = update_lights_cb
self.bridge_type = bridge_type
self.allow_unreachable = allow_unreachable
self.is_group = is_group
@@ -381,14 +310,15 @@ class HueLight(Light):
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_XY_COLOR in kwargs:
if self.info.get('manufacturername') == "OSRAM":
hue, sat = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
command['hue'] = hue
if self.info.get('manufacturername') == 'OSRAM':
color_hue, sat = color_util.color_xy_to_hs(
*kwargs[ATTR_XY_COLOR])
command['hue'] = color_hue
command['sat'] = sat
else:
command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs:
if self.info.get('manufacturername') == "OSRAM":
if self.info.get('manufacturername') == 'OSRAM':
hsv = color_util.color_RGB_to_hsv(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['hue'] = hsv[0]
+35 -47
View File
@@ -33,7 +33,7 @@ import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['aiolifx==0.6.0', 'aiolifx_effects==0.1.2']
REQUIREMENTS = ['aiolifx==0.6.1', 'aiolifx_effects==0.1.2']
UDP_BROADCAST_PORT = 56700
@@ -157,20 +157,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return True
def lifxwhite(device):
"""Return whether this is a white-only bulb."""
features = aiolifx().products.features_map.get(device.product, None)
if features:
return not features["color"]
return False
def lifxmultizone(device):
"""Return whether this is a multizone bulb/strip."""
features = aiolifx().products.features_map.get(device.product, None)
if features:
return features["multizone"]
return False
def lifx_features(device):
"""Return a feature map for this device, or a default map if unknown."""
return aiolifx().products.features_map.get(device.product) or \
aiolifx().products.features_map.get(1)
def find_hsbk(**kwargs):
@@ -342,12 +332,12 @@ class LIFXManager(object):
device.retry_count = MESSAGE_RETRIES
device.unregister_timeout = UNAVAILABLE_GRACE
if lifxwhite(device):
entity = LIFXWhite(device, self.effects_conductor)
elif lifxmultizone(device):
if lifx_features(device)["multizone"]:
entity = LIFXStrip(device, self.effects_conductor)
else:
elif lifx_features(device)["color"]:
entity = LIFXColor(device, self.effects_conductor)
else:
entity = LIFXWhite(device, self.effects_conductor)
_LOGGER.debug("%s register READY", entity.who)
self.entities[device.mac_addr] = entity
@@ -427,6 +417,29 @@ class LIFXLight(Light):
"""Return a string identifying the device."""
return "%s (%s)" % (self.device.ip_addr, self.name)
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
kelvin = lifx_features(self.device)['max_kelvin']
return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin))
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
kelvin = lifx_features(self.device)['min_kelvin']
return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin))
@property
def supported_features(self):
"""Flag supported features."""
support = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_EFFECT
device_features = lifx_features(self.device)
if device_features['min_kelvin'] != device_features['max_kelvin']:
support |= SUPPORT_COLOR_TEMP
return support
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
@@ -571,22 +584,6 @@ class LIFXLight(Light):
class LIFXWhite(LIFXLight):
"""Representation of a white-only LIFX light."""
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return math.floor(color_util.color_temperature_kelvin_to_mired(6500))
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return math.ceil(color_util.color_temperature_kelvin_to_mired(2700))
@property
def supported_features(self):
"""Flag supported features."""
return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
SUPPORT_EFFECT)
@property
def effect_list(self):
"""Return the list of supported effects for this light."""
@@ -599,21 +596,12 @@ class LIFXWhite(LIFXLight):
class LIFXColor(LIFXLight):
"""Representation of a color LIFX light."""
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return math.floor(color_util.color_temperature_kelvin_to_mired(9000))
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return math.ceil(color_util.color_temperature_kelvin_to_mired(2500))
@property
def supported_features(self):
"""Flag supported features."""
return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
SUPPORT_EFFECT | SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR)
support = super().supported_features
support |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR
return support
@property
def effect_list(self):
+46 -8
View File
@@ -12,13 +12,15 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA)
from homeassistant.components import mochad
from homeassistant.const import (CONF_NAME, CONF_PLATFORM, CONF_DEVICES,
CONF_ADDRESS)
from homeassistant.const import (
CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS)
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['mochad']
_LOGGER = logging.getLogger(__name__)
CONF_BRIGHTNESS_LEVELS = 'brightness_levels'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): mochad.DOMAIN,
@@ -26,6 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_ADDRESS): cv.x10_address,
vol.Optional(mochad.CONF_COMM_TYPE): cv.string,
vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32):
vol.All(vol.Coerce(int), vol.In([32, 64, 256])),
}]
})
@@ -54,6 +58,7 @@ class MochadLight(Light):
comm_type=self._comm_type)
self._brightness = 0
self._state = self._get_device_status()
self._brightness_levels = dev.get(CONF_BRIGHTNESS_LEVELS) - 1
@property
def brightness(self):
@@ -62,7 +67,8 @@ class MochadLight(Light):
def _get_device_status(self):
"""Get the status of the light from mochad."""
status = self.device.get_status().rstrip()
with mochad.REQ_LOCK:
status = self.device.get_status().rstrip()
return status == 'on'
@property
@@ -85,15 +91,47 @@ class MochadLight(Light):
"""X10 devices are normally 1-way so we have to assume the state."""
return True
def _calculate_brightness_value(self, value):
return int(value * (float(self._brightness_levels) / 255.0))
def _adjust_brightness(self, brightness):
if self._brightness > brightness:
bdelta = self._brightness - brightness
mochad_brightness = self._calculate_brightness_value(bdelta)
self.device.send_cmd("dim {}".format(mochad_brightness))
self._controller.read_data()
elif self._brightness < brightness:
bdelta = brightness - self._brightness
mochad_brightness = self._calculate_brightness_value(bdelta)
self.device.send_cmd("bright {}".format(mochad_brightness))
self._controller.read_data()
def turn_on(self, **kwargs):
"""Send the command to turn the light on."""
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
self.device.send_cmd("xdim {}".format(self._brightness))
self._controller.read_data()
brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
with mochad.REQ_LOCK:
if self._brightness_levels > 32:
out_brightness = self._calculate_brightness_value(brightness)
self.device.send_cmd('xdim {}'.format(out_brightness))
self._controller.read_data()
else:
self.device.send_cmd("on")
self._controller.read_data()
# There is no persistence for X10 modules so a fresh on command
# will be full brightness
if self._brightness == 0:
self._brightness = 255
self._adjust_brightness(brightness)
self._brightness = brightness
self._state = True
def turn_off(self, **kwargs):
"""Send the command to turn the light on."""
self.device.send_cmd('off')
self._controller.read_data()
with mochad.REQ_LOCK:
self.device.send_cmd('off')
self._controller.read_data()
# There is no persistence for X10 modules so we need to prepare
# to track a fresh on command will full brightness
if self._brightness_levels == 31:
self._brightness = 0
self._state = False
+10 -1
View File
@@ -72,6 +72,7 @@ class TPLinkSmartBulb(Light):
if name is not None:
self._name = name
self._state = None
self._available = True
self._color_temp = None
self._brightness = None
self._rgb = None
@@ -83,6 +84,11 @@ class TPLinkSmartBulb(Light):
"""Return the name of the Smart Bulb, if any."""
return self._name
@property
def available(self) -> bool:
"""Return if bulb is available."""
return self._available
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
@@ -132,6 +138,7 @@ class TPLinkSmartBulb(Light):
"""Update the TP-Link Bulb's state."""
from pyHS100 import SmartDeviceException
try:
self._available = True
if self._supported_features == 0:
self.get_features()
self._state = (
@@ -163,8 +170,10 @@ class TPLinkSmartBulb(Light):
except KeyError:
# device returned no daily/monthly history
pass
except (SmartDeviceException, OSError) as ex:
_LOGGER.warning('Could not read state for %s: %s', self._name, ex)
_LOGGER.warning("Could not read state for %s: %s", self._name, ex)
self._available = False
@property
def supported_features(self):
+24 -17
View File
@@ -99,7 +99,7 @@ class TradfriGroup(Light):
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Instruct the group lights to turn off."""
self.hass.async_add_job(self._api(self._group.set_state(0)))
yield from self._api(self._group.set_state(0))
@asyncio.coroutine
def async_turn_on(self, **kwargs):
@@ -112,10 +112,10 @@ class TradfriGroup(Light):
if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254
self.hass.async_add_job(self._api(
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)))
yield from self._api(
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
else:
self.hass.async_add_job(self._api(self._group.set_state(1)))
yield from self._api(self._group.set_state(1))
@callback
def _async_start_observe(self, exc=None):
@@ -140,11 +140,11 @@ class TradfriGroup(Light):
self._group = group
self._name = group.name
@callback
def _observe_update(self, tradfri_device):
"""Receive new state data for this light."""
self._refresh(tradfri_device)
self.hass.async_add_job(self.async_update_ha_state())
self.async_schedule_update_ha_state()
class TradfriLight(Light):
@@ -160,6 +160,7 @@ class TradfriLight(Light):
self._rgb_color = None
self._features = SUPPORTED_FEATURES
self._temp_supported = False
self._available = True
self._refresh(light)
@@ -196,6 +197,11 @@ class TradfriLight(Light):
"""Start thread when added to hass."""
self._async_start_observe()
@property
def available(self):
"""Return True if entity is available."""
return self._available
@property
def should_poll(self):
"""No polling needed for tradfri light."""
@@ -238,8 +244,7 @@ class TradfriLight(Light):
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
self.hass.async_add_job(self._api(
self._light_control.set_state(False)))
yield from self._api(self._light_control.set_state(False))
@asyncio.coroutine
def async_turn_on(self, **kwargs):
@@ -250,17 +255,17 @@ class TradfriLight(Light):
for ATTR_RGB_COLOR, this also supports Philips Hue bulbs.
"""
if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None:
self.hass.async_add_job(self._api(
yield from self._api(
self._light.light_control.set_rgb_color(
*kwargs[ATTR_RGB_COLOR])))
*kwargs[ATTR_RGB_COLOR]))
elif ATTR_COLOR_TEMP in kwargs and \
self._light_data.hex_color is not None and \
self._temp_supported:
kelvin = color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP])
self.hass.async_add_job(self._api(
self._light_control.set_kelvin_color(kelvin)))
yield from self._api(
self._light_control.set_kelvin_color(kelvin))
keys = {}
if ATTR_TRANSITION in kwargs:
@@ -270,12 +275,12 @@ class TradfriLight(Light):
if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254
self.hass.async_add_job(self._api(
yield from self._api(
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS],
**keys)))
**keys))
else:
self.hass.async_add_job(self._api(
self._light_control.set_state(True)))
yield from self._api(
self._light_control.set_state(True))
@callback
def _async_start_observe(self, exc=None):
@@ -300,6 +305,7 @@ class TradfriLight(Light):
self._light = light
# Caching of LightControl and light object
self._available = light.reachable
self._light_control = light.light_control
self._light_data = light.light_control.lights[0]
self._name = light.name
@@ -318,10 +324,11 @@ class TradfriLight(Light):
self._temp_supported = self._light.device_info.manufacturer \
in ALLOWED_TEMPERATURES
@callback
def _observe_update(self, tradfri_device):
"""Receive new state data for this light."""
self._refresh(tradfri_device)
self._rgb_color = color_util.rgb_hex_to_rgb_list(
self._light_data.hex_color_inferred
)
self.hass.async_add_job(self.async_update_ha_state())
self.async_schedule_update_ha_state()
+2 -1
View File
@@ -21,7 +21,8 @@ DEPENDENCIES = ['vera']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera lights."""
add_devices(
VeraLight(device, VERA_CONTROLLER) for device in VERA_DEVICES['light'])
VeraLight(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['light'])
class VeraLight(VeraDevice, Light):
+4 -1
View File
@@ -66,7 +66,10 @@ class ISYLockDevice(isy.ISYDevice, LockDevice):
@property
def state(self) -> str:
"""Get the state of the lock."""
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
if self.is_unknown():
return None
else:
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
def lock(self, **kwargs) -> None:
"""Send the lock command to the ISY994 device."""
+2 -2
View File
@@ -19,8 +19,8 @@ DEPENDENCIES = ['vera']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Vera locks."""
add_devices(
VeraLock(device, VERA_CONTROLLER) for
device in VERA_DEVICES['lock'])
VeraLock(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['lock'])
class VeraLock(VeraDevice, LockDevice):
+5 -5
View File
@@ -135,9 +135,8 @@ class LogbookView(HomeAssistantView):
hass = request.app['hass']
events = yield from hass.async_add_job(
_get_events, hass, start_day, end_day)
events = _exclude_events(events, self.config)
return self.json(humanify(events))
_get_events, hass, self.config, start_day, end_day)
return self.json(events)
class Entry(object):
@@ -274,7 +273,7 @@ def humanify(events):
entity_id)
def _get_events(hass, start_day, end_day):
def _get_events(hass, config, start_day, end_day):
"""Get events for a period of time."""
from homeassistant.components.recorder.models import Events
from homeassistant.components.recorder.util import (
@@ -285,7 +284,8 @@ def _get_events(hass, start_day, end_day):
Events.time_fired).filter(
(Events.time_fired > start_day) &
(Events.time_fired < end_day))
return execute(query)
events = execute(query)
return humanify(_exclude_events(events, config))
def _exclude_events(events, config):
+1 -1
View File
@@ -16,7 +16,7 @@ from homeassistant.components.media_player import (
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2017.11.26']
REQUIREMENTS = ['youtube_dl==2017.12.10']
_LOGGER = logging.getLogger(__name__)
@@ -20,7 +20,9 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pychromecast==1.0.2']
# Do not upgrade to 1.0.2, it breaks a bunch of stuff
# https://github.com/home-assistant/home-assistant/issues/10926
REQUIREMENTS = ['pychromecast==0.8.2']
_LOGGER = logging.getLogger(__name__)
@@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.5.4']
REQUIREMENTS = ['denonavr==0.5.5']
_LOGGER = logging.getLogger(__name__)
@@ -102,12 +102,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if config.get(CONF_HOST) is None and discovery_info is None:
d_receivers = denonavr.discover()
# More than one receiver could be discovered by that method
if d_receivers is not None:
for d_receiver in d_receivers:
host = d_receiver["host"]
name = d_receiver["friendlyName"]
new_hosts.append(
NewHost(host=host, name=name))
for d_receiver in d_receivers:
host = d_receiver["host"]
name = d_receiver["friendlyName"]
new_hosts.append(
NewHost(host=host, name=name))
for entry in new_hosts:
# Check if host not in cache, append it and save for later
@@ -20,8 +20,9 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, STATE_ON, STATE_OFF, STATE_PLAYING,
STATE_PAUSED, CONF_NAME)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['liveboxplaytv==2.0.0']
REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.3']
_LOGGER = logging.getLogger(__name__)
@@ -76,19 +77,32 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
self._channel_list = {}
self._current_channel = None
self._current_program = None
self._media_duration = None
self._media_remaining_time = None
self._media_image_url = None
self._media_last_updated = None
@asyncio.coroutine
def async_update(self):
"""Retrieve the latest data."""
import pyteleloisirs
try:
self._state = self.refresh_state()
# Update current channel
channel = self._client.channel
if channel is not None:
self._current_program = yield from \
self._client.async_get_current_program_name()
self._current_channel = channel
program = yield from \
self._client.async_get_current_program()
if program and self._current_program != program.get('name'):
self._current_program = program.get('name')
# Media progress info
self._media_duration = \
pyteleloisirs.get_program_duration(program)
rtime = pyteleloisirs.get_remaining_time(program)
if rtime != self._media_remaining_time:
self._media_remaining_time = rtime
self._media_last_updated = dt_util.utcnow()
# Set media image to current program if a thumbnail is
# available. Otherwise we'll use the channel's image.
img_size = 800
@@ -100,7 +114,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
chan_img_url = \
self._client.get_current_channel_image(img_size)
self._media_image_url = chan_img_url
self.refresh_channel_list()
except requests.ConnectionError:
self._state = None
@@ -149,8 +162,25 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
if self._current_program:
return '{}: {}'.format(self._current_channel,
self._current_program)
else:
return self._current_channel
return self._current_channel
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._media_duration
@property
def media_position(self):
"""Position of current playing media in seconds."""
return self._media_remaining_time
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return self._media_last_updated
@property
def supported_features(self):
@@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player.samsungtv/
"""
import logging
import socket
from datetime import timedelta
import voluptuous as vol
@@ -17,6 +18,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT,
CONF_MAC)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt as dt_util
REQUIREMENTS = ['samsungctl==0.6.0', 'wakeonlan==0.2.2']
@@ -100,6 +102,9 @@ class SamsungTVDevice(MediaPlayerDevice):
self._playing = True
self._state = STATE_UNKNOWN
self._remote = None
# Mark the end of a shutdown command (need to wait 15 seconds before
# sending the next command to avoid turning the TV back ON).
self._end_of_power_off = None
# Generate a configuration for the Samsung library
self._config = {
'name': 'HomeAssistant',
@@ -118,7 +123,7 @@ class SamsungTVDevice(MediaPlayerDevice):
def update(self):
"""Retrieve the latest data."""
# Send an empty key to see if we are still connected
return self.send_key('KEY')
self.send_key('KEY')
def get_remote(self):
"""Create or return a remote control instance."""
@@ -130,6 +135,10 @@ class SamsungTVDevice(MediaPlayerDevice):
def send_key(self, key):
"""Send a key to the tv and handles exceptions."""
if self._power_off_in_progress() \
and not (key == 'KEY_POWER' or key == 'KEY_POWEROFF'):
_LOGGER.info("TV is powering off, not sending command: %s", key)
return
try:
self.get_remote().control(key)
self._state = STATE_ON
@@ -139,13 +148,16 @@ class SamsungTVDevice(MediaPlayerDevice):
# BrokenPipe can occur when the commands is sent to fast
self._state = STATE_ON
self._remote = None
return False
return
except (self._exceptions_class.ConnectionClosed, OSError):
self._state = STATE_OFF
self._remote = None
return False
if self._power_off_in_progress():
self._state = STATE_OFF
return True
def _power_off_in_progress(self):
return self._end_of_power_off is not None and \
self._end_of_power_off > dt_util.utcnow()
@property
def name(self):
@@ -171,12 +183,17 @@ class SamsungTVDevice(MediaPlayerDevice):
def turn_off(self):
"""Turn off media player."""
self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15)
if self._config['method'] == 'websocket':
self.send_key('KEY_POWER')
else:
self.send_key('KEY_POWEROFF')
# Force closing of remote session to provide instant UI feedback
self.get_remote().close()
try:
self.get_remote().close()
except OSError:
_LOGGER.debug("Could not establish connection.")
def volume_up(self):
"""Volume up the media player."""
@@ -215,6 +215,18 @@ sonos_clear_sleep_timer:
description: Name(s) of entities that will have the timer cleared.
example: 'media_player.living_room_sonos'
sonos_set_option:
description: Set Sonos sound options.
fields:
entity_id:
description: Name(s) of entities that will have options set.
example: 'media_player.living_room_sonos'
night_sound:
description: Enable Night Sound mode
example: 'true'
speech_enhance:
description: Enable Speech Enhancement mode
example: 'true'
soundtouch_play_everywhere:
description: Play on all Bose Soundtouch devices.
+77 -7
View File
@@ -19,7 +19,7 @@ from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP,
SUPPORT_PLAY)
SUPPORT_PLAY, SUPPORT_SHUFFLE_SET)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
CONF_HOSTS, ATTR_TIME)
@@ -27,7 +27,7 @@ from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
REQUIREMENTS = ['SoCo==0.12']
REQUIREMENTS = ['SoCo==0.13']
_LOGGER = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ _REQUESTS_LOGGER.setLevel(logging.ERROR)
SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET
SERVICE_JOIN = 'sonos_join'
SERVICE_UNJOIN = 'sonos_unjoin'
@@ -52,6 +52,7 @@ SERVICE_RESTORE = 'sonos_restore'
SERVICE_SET_TIMER = 'sonos_set_sleep_timer'
SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
SERVICE_SET_OPTION = 'sonos_set_option'
DATA_SONOS = 'sonos'
@@ -69,6 +70,8 @@ ATTR_ENABLED = 'enabled'
ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones'
ATTR_MASTER = 'master'
ATTR_WITH_GROUP = 'with_group'
ATTR_NIGHT_SOUND = 'night_sound'
ATTR_SPEECH_ENHANCE = 'speech_enhance'
ATTR_IS_COORDINATOR = 'is_coordinator'
@@ -105,6 +108,11 @@ SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({
vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean,
})
SONOS_SET_OPTION_SCHEMA = SONOS_SCHEMA.extend({
vol.Optional(ATTR_NIGHT_SOUND): cv.boolean,
vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Sonos platform."""
@@ -140,7 +148,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hosts = hosts.split(',') if isinstance(hosts, str) else hosts
players = []
for host in hosts:
players.append(soco.SoCo(socket.gethostbyname(host)))
try:
players.append(soco.SoCo(socket.gethostbyname(host)))
except OSError:
_LOGGER.warning("Failed to initialize '%s'", host)
if not players:
players = soco.discover(
@@ -189,6 +200,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device.clear_sleep_timer()
elif service.service == SERVICE_UPDATE_ALARM:
device.update_alarm(**service.data)
elif service.service == SERVICE_SET_OPTION:
device.update_option(**service.data)
device.schedule_update_ha_state(True)
@@ -221,6 +234,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
descriptions.get(SERVICE_UPDATE_ALARM),
schema=SONOS_UPDATE_ALARM_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_SET_OPTION, service_handle,
descriptions.get(SERVICE_SET_OPTION),
schema=SONOS_SET_OPTION_SCHEMA)
def _parse_timespan(timespan):
"""Parse a time-span into number of seconds."""
@@ -331,8 +349,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
self._support_shuffle_set = True
self._support_stop = False
self._support_pause = False
self._night_sound = None
self._speech_enhance = None
self._current_track_uri = None
self._current_track_is_radio_stream = False
self._queue = None
@@ -450,8 +471,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
self._support_shuffle_set = False
self._support_stop = False
self._support_pause = False
self._night_sound = None
self._speech_enhance = None
self._is_playing_tv = False
self._is_playing_line_in = False
self._source_name = None
@@ -524,6 +548,9 @@ class SonosDevice(MediaPlayerDevice):
media_position_updated_at = None
source_name = None
night_sound = self._player.night_mode
speech_enhance = self._player.dialog_mode
is_radio_stream = \
current_media_uri.startswith('x-sonosapi-stream:') or \
current_media_uri.startswith('x-rincon-mp3radio:')
@@ -536,6 +563,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = False
support_stop = True
support_pause = False
support_shuffle_set = False
if is_playing_tv:
media_artist = SUPPORT_SOURCE_TV
@@ -558,6 +586,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = True
support_stop = True
support_pause = False
support_shuffle_set = False
source_name = 'Radio'
# Check if currently playing radio station is in favorites
@@ -622,6 +651,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = True
support_stop = True
support_pause = True
support_shuffle_set = True
position_info = self._player.avTransport.GetPositionInfo(
[('InstanceID', 0),
@@ -694,8 +724,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = support_previous_track
self._support_next_track = support_next_track
self._support_play = support_play
self._support_shuffle_set = support_shuffle_set
self._support_stop = support_stop
self._support_pause = support_pause
self._night_sound = night_sound
self._speech_enhance = speech_enhance
self._is_playing_tv = is_playing_tv
self._is_playing_line_in = is_playing_line_in
self._source_name = source_name
@@ -762,6 +795,11 @@ class SonosDevice(MediaPlayerDevice):
"""Return true if volume is muted."""
return self._player_volume_muted
@property
def shuffle(self):
"""Shuffling state."""
return True if self._player.play_mode == 'SHUFFLE' else False
@property
def media_content_id(self):
"""Content ID of current playing media."""
@@ -834,6 +872,16 @@ class SonosDevice(MediaPlayerDevice):
return self._media_title
@property
def night_sound(self):
"""Get status of Night Sound."""
return self._night_sound
@property
def speech_enhance(self):
"""Get status of Speech Enhancement."""
return self._speech_enhance
@property
def supported_features(self):
"""Flag media player features that are supported."""
@@ -850,7 +898,8 @@ class SonosDevice(MediaPlayerDevice):
if not self._support_play:
supported = supported ^ SUPPORT_PLAY
if not self._support_shuffle_set:
supported = supported ^ SUPPORT_SHUFFLE_SET
if not self._support_stop:
supported = supported ^ SUPPORT_STOP
@@ -874,6 +923,11 @@ class SonosDevice(MediaPlayerDevice):
"""Set volume level, range 0..1."""
self._player.volume = str(int(volume * 100))
@soco_error
def set_shuffle(self, shuffle):
"""Enable/Disable shuffle mode."""
self._player.play_mode = 'SHUFFLE' if shuffle else 'NORMAL'
@soco_error
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
@@ -932,7 +986,6 @@ class SonosDevice(MediaPlayerDevice):
self._player.stop()
self._player.clear_queue()
self._player.play_mode = 'NORMAL'
self._player.add_to_queue(didl)
@property
@@ -1160,7 +1213,24 @@ class SonosDevice(MediaPlayerDevice):
a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
a.save()
@soco_error
def update_option(self, **data):
"""Modify playback options."""
if ATTR_NIGHT_SOUND in data and self.night_sound is not None:
self.soco.night_mode = data[ATTR_NIGHT_SOUND]
if ATTR_SPEECH_ENHANCE in data and self.speech_enhance is not None:
self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE]
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
return {ATTR_IS_COORDINATOR: self.is_coordinator}
attributes = {ATTR_IS_COORDINATOR: self.is_coordinator}
if self.night_sound is not None:
attributes[ATTR_NIGHT_SOUND] = self.night_sound
if self.speech_enhance is not None:
attributes[ATTR_SPEECH_ENHANCE] = self.speech_enhance
return attributes
@@ -0,0 +1,207 @@
"""
Support for Logitech UE Smart Radios.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.ue_smart_radio/
"""
import logging
import voluptuous as vol
import requests
from homeassistant.components.media_player import (
MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_MUTE)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, STATE_OFF, STATE_IDLE, STATE_PLAYING,
STATE_PAUSED)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:radio"
URL = "http://decibel.logitechmusic.com/jsonrpc.js"
SUPPORT_UE_SMART_RADIO = SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_TURN_ON | \
SUPPORT_TURN_OFF | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
PLAYBACK_DICT = {"play": STATE_PLAYING,
"pause": STATE_PAUSED,
"stop": STATE_IDLE}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def send_request(payload, session):
"""Send request to radio."""
try:
request = requests.post(URL,
cookies={"sdi_squeezenetwork_session":
session},
json=payload, timeout=5)
except requests.exceptions.Timeout:
_LOGGER.error("Timed out when sending request")
except requests.exceptions.ConnectionError:
_LOGGER.error("An error occurred while connecting")
else:
return request.json()
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Logitech UE Smart Radio platform."""
email = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
session_request = requests.post("https://www.uesmartradio.com/user/login",
data={"email": email, "password":
password})
session = session_request.cookies["sdi_squeezenetwork_session"]
player_request = send_request({"params": ["", ["serverstatus"]]}, session)
player_id = player_request["result"]["players_loop"][0]["playerid"]
player_name = player_request["result"]["players_loop"][0]["name"]
add_devices([UERadioDevice(session, player_id, player_name)])
class UERadioDevice(MediaPlayerDevice):
"""Representation of a Logitech UE Smart Radio device."""
def __init__(self, session, player_id, player_name):
"""Initialize the Logitech UE Smart Radio device."""
self._session = session
self._player_id = player_id
self._name = player_name
self._state = None
self._volume = 0
self._last_volume = 0
self._media_title = None
self._media_artist = None
self._media_artwork_url = None
def send_command(self, command):
"""Send command to radio."""
send_request({"method": "slim.request", "params":
[self._player_id, command]}, self._session)
def update(self):
"""Get the latest details from the device."""
request = send_request({
"method": "slim.request", "params":
[self._player_id, ["status", "-", 1,
"tags:cgABbehldiqtyrSuoKLN"]]}, self._session)
if request["error"] is not None:
self._state = None
return
if request["result"]["power"] == 0:
self._state = STATE_OFF
else:
self._state = PLAYBACK_DICT[request["result"]["mode"]]
media_info = request["result"]["playlist_loop"][0]
self._volume = request["result"]["mixer volume"] / 100
self._media_artwork_url = media_info["artwork_url"]
self._media_title = media_info["title"]
if "artist" in media_info:
self._media_artist = media_info["artist"]
else:
self._media_artist = media_info.get("remote_title")
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return ICON
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return True if self._volume <= 0 else False
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume
@property
def supported_features(self):
"""Flag of features that are supported."""
return SUPPORT_UE_SMART_RADIO
@property
def media_content_type(self):
"""Return the media content type."""
return MEDIA_TYPE_MUSIC
@property
def media_image_url(self):
"""Image URL of current playing media."""
return self._media_artwork_url
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self._media_artist
@property
def media_title(self):
"""Title of current playing media."""
return self._media_title
def turn_on(self):
"""Turn on specified media player or all."""
self.send_command(["power", 1])
def turn_off(self):
"""Turn off specified media player or all."""
self.send_command(["power", 0])
def media_play(self):
"""Send the media player the command for play/pause."""
self.send_command(["play"])
def media_pause(self):
"""Send the media player the command for pause."""
self.send_command(["pause"])
def media_stop(self):
"""Send the media player the stop command."""
self.send_command(["stop"])
def media_previous_track(self):
"""Send the media player the command for prev track."""
self.send_command(["button", "rew"])
def media_next_track(self):
"""Send the media player the command for next track."""
self.send_command(["button", "fwd"])
def mute_volume(self, mute):
"""Send mute command."""
if mute:
self._last_volume = self._volume
self.send_command(["mixer", "volume", 0])
else:
self.send_command(["mixer", "volume", self._last_volume * 100])
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self.send_command(["mixer", "volume", volume * 100])
@@ -322,12 +322,15 @@ class LgWebOSDevice(MediaPlayerDevice):
def select_source(self, source):
"""Select input source."""
if self._source_list.get(source).get('title'):
self._current_source_id = self._source_list[source]['id']
source = self._source_list.get(source)
if source is None:
_LOGGER.warning("Source %s not found for %s", source, self.name)
return
self._current_source_id = self._source_list[source]['id']
if source.get('title'):
self._current_source = self._source_list[source]['title']
self._client.launch_app(self._source_list[source]['id'])
elif self._source_list.get(source).get('label'):
self._current_source_id = self._source_list[source]['id']
elif source.get('label'):
self._current_source = self._source_list[source]['label']
self._client.set_input(self._source_list[source]['id'])
@@ -36,7 +36,7 @@ SUPPORTED_FEATURES = (
KNOWN_HOSTS_KEY = 'data_yamaha_musiccast'
INTERVAL_SECONDS = 'interval_seconds'
REQUIREMENTS = ['pymusiccast==0.1.5']
REQUIREMENTS = ['pymusiccast==0.1.6']
DEFAULT_PORT = 5005
DEFAULT_INTERVAL = 480
@@ -0,0 +1,174 @@
"""
Support for interface with a Ziggo Mediabox XL.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.ziggo_mediabox_xl/
"""
import logging
import socket
import voluptuous as vol
from homeassistant.components.media_player import (
PLATFORM_SCHEMA, MediaPlayerDevice,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE,
SUPPORT_PLAY, SUPPORT_PAUSE)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['ziggo-mediabox-xl==1.0.0']
_LOGGER = logging.getLogger(__name__)
DATA_KNOWN_DEVICES = 'ziggo_mediabox_xl_known_devices'
SUPPORT_ZIGGO = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Ziggo Mediabox XL platform."""
from ziggo_mediabox_xl import ZiggoMediaboxXL
hass.data[DATA_KNOWN_DEVICES] = known_devices = set()
# Is this a manual configuration?
if config.get(CONF_HOST) is not None:
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
elif discovery_info is not None:
host = discovery_info.get('host')
name = discovery_info.get('name')
else:
_LOGGER.error("Cannot determine device")
return
# Only add a device once, so discovered devices do not override manual
# config.
hosts = []
ip_addr = socket.gethostbyname(host)
if ip_addr not in known_devices:
try:
mediabox = ZiggoMediaboxXL(ip_addr)
if mediabox.test_connection():
hosts.append(ZiggoMediaboxXLDevice(mediabox, host, name))
known_devices.add(ip_addr)
else:
_LOGGER.error("Can't connect to %s", host)
except socket.error as error:
_LOGGER.error("Can't connect to %s: %s", host, error)
else:
_LOGGER.info("Ignoring duplicate Ziggo Mediabox XL %s", host)
add_devices(hosts, True)
class ZiggoMediaboxXLDevice(MediaPlayerDevice):
"""Representation of a Ziggo Mediabox XL Device."""
def __init__(self, mediabox, host, name):
"""Initialize the device."""
# Generate a configuration for the Samsung library
self._mediabox = mediabox
self._host = host
self._name = name
self._state = None
def update(self):
"""Retrieve the state of the device."""
try:
if self._mediabox.turned_on():
if self._state != STATE_PAUSED:
self._state = STATE_PLAYING
else:
self._state = STATE_OFF
except socket.error:
_LOGGER.error("Couldn't fetch state from %s", self._host)
def send_keys(self, keys):
"""Send keys to the device and handle exceptions."""
try:
self._mediabox.send_keys(keys)
except socket.error:
_LOGGER.error("Couldn't send keys to %s", self._host)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def source_list(self):
"""List of available sources (channels)."""
return [self._mediabox.channels()[c]
for c in sorted(self._mediabox.channels().keys())]
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_ZIGGO
def turn_on(self):
"""Turn the media player on."""
self.send_keys(['POWER'])
self._state = STATE_ON
def turn_off(self):
"""Turn off media player."""
self.send_keys(['POWER'])
self._state = STATE_OFF
def media_play(self):
"""Send play command."""
self.send_keys(['PLAY'])
self._state = STATE_PLAYING
def media_pause(self):
"""Send pause command."""
self.send_keys(['PAUSE'])
self._state = STATE_PAUSED
def media_play_pause(self):
"""Simulate play pause media player."""
self.send_keys(['PAUSE'])
if self._state == STATE_PAUSED:
self._state = STATE_PLAYING
else:
self._state = STATE_PAUSED
def media_next_track(self):
"""Channel up."""
self.send_keys(['CHAN_UP'])
self._state = STATE_PLAYING
def media_previous_track(self):
"""Channel down."""
self.send_keys(['CHAN_DOWN'])
self._state = STATE_PLAYING
def select_source(self, source):
"""Select the channel."""
if str(source).isdigit():
digits = str(source)
else:
digits = next((
key for key, value in self._mediabox.channels().items()
if value == source), None)
if digits is None:
return
self.send_keys(['NUM_{}'.format(digit)
for digit in str(digits)])
self._state = STATE_PLAYING
+3
View File
@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mochad/
"""
import logging
import threading
import voluptuous as vol
@@ -23,6 +24,8 @@ CONF_COMM_TYPE = 'comm_type'
DOMAIN = 'mochad'
REQ_LOCK = threading.Lock()
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST, default='localhost'): cv.string,
+2 -2
View File
@@ -17,8 +17,8 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.3.zip'
'#pybotvac==0.0.3']
REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.4.zip'
'#pybotvac==0.0.4']
DOMAIN = 'neato'
NEATO_ROBOTS = 'neato_robots'
@@ -4,8 +4,9 @@ Notifications for Android TV notification service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.nfandroidtv/
"""
import os
import logging
import io
import base64
import requests
import voluptuous as vol
@@ -31,6 +32,9 @@ DEFAULT_TRANSPARENCY = 'default'
DEFAULT_COLOR = 'grey'
DEFAULT_INTERRUPT = False
DEFAULT_TIMEOUT = 5
DEFAULT_ICON = (
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApo'
'cMXEAAAAASUVORK5CYII=')
ATTR_DURATION = 'duration'
ATTR_POSITION = 'position'
@@ -110,16 +114,13 @@ class NFAndroidTVNotificationService(BaseNotificationService):
self._default_color = color
self._default_interrupt = interrupt
self._timeout = timeout
self._icon_file = os.path.join(
os.path.dirname(__file__), '..', 'frontend', 'www_static', 'icons',
'favicon-192x192.png')
self._icon_file = io.BytesIO(base64.b64decode(DEFAULT_ICON))
def send_message(self, message="", **kwargs):
"""Send a message to a Android TV device."""
_LOGGER.debug("Sending notification to: %s", self._target)
payload = dict(filename=('icon.png',
open(self._icon_file, 'rb'),
payload = dict(filename=('icon.png', self._icon_file,
'application/octet-stream',
{'Expires': '0'}), type='0',
title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
@@ -129,7 +130,7 @@ class NFAndroidTVNotificationService(BaseNotificationService):
transparency='%i' % TRANSPARENCIES.get(
self._default_transparency),
offset='0', app=ATTR_TITLE_DEFAULT, force='true',
interrupt='%i' % self._default_interrupt)
interrupt='%i' % self._default_interrupt,)
data = kwargs.get(ATTR_DATA)
if data:
+47 -5
View File
@@ -14,12 +14,13 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components import recorder
from homeassistant.const import (
CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, TEMP_CELSIUS,
EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN)
EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN,
ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT)
from homeassistant import core as hacore
from homeassistant.helpers import state as state_helper
from homeassistant.util.temperature import fahrenheit_to_celsius
REQUIREMENTS = ['prometheus_client==0.0.19']
REQUIREMENTS = ['prometheus_client==0.0.21']
_LOGGER = logging.getLogger(__name__)
@@ -159,6 +160,26 @@ class Metrics(object):
value = state_helper.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
def _handle_climate(self, state):
temp = state.attributes.get(ATTR_TEMPERATURE)
if temp:
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if unit == TEMP_FAHRENHEIT:
temp = fahrenheit_to_celsius(temp)
metric = self._metric(
'temperature_c', self.prometheus_client.Gauge,
'Temperature in degrees Celsius')
metric.labels(**self._labels(state)).set(temp)
metric = self._metric(
'climate_state', self.prometheus_client.Gauge,
'State of the thermostat (0/1)')
try:
value = state_helper.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
def _handle_sensor(self, state):
_sensor_types = {
TEMP_CELSIUS: (
@@ -189,9 +210,17 @@ class Metrics(object):
'electricity_usage_w', self.prometheus_client.Gauge,
'Currently reported electricity draw in Watts',
),
'min': (
'sensor_min', self.prometheus_client.Gauge,
'Time in minutes reported by a sensor'
),
'Events': (
'sensor_event_count', self.prometheus_client.Gauge,
'Number of events for a sensor'
),
}
unit = state.attributes.get('unit_of_measurement')
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
metric = _sensor_types.get(unit)
if metric is not None:
@@ -212,12 +241,25 @@ class Metrics(object):
self.prometheus_client.Gauge,
'State of the switch (0/1)',
)
value = state_helper.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
try:
value = state_helper.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
def _handle_zwave(self, state):
self._battery(state)
def _handle_automation(self, state):
metric = self._metric(
'automation_triggered_count',
self.prometheus_client.Counter,
'Count of times an automation has been triggered',
)
metric.labels(**self._labels(state)).inc()
class PrometheusView(HomeAssistantView):
"""Handle Prometheus requests."""
+60
View File
@@ -0,0 +1,60 @@
"""
Support for Vera scenes.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/scene.vera/
"""
import logging
from homeassistant.util import slugify
from homeassistant.components.scene import Scene
from homeassistant.components.vera import (
VERA_CONTROLLER, VERA_SCENES, VERA_ID_FORMAT)
DEPENDENCIES = ['vera']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera scenes."""
add_devices(
[VeraScene(scene, hass.data[VERA_CONTROLLER])
for scene in hass.data[VERA_SCENES]], True)
class VeraScene(Scene):
"""Representation of a Vera scene entity."""
def __init__(self, vera_scene, controller):
"""Initialize the scene."""
self.vera_scene = vera_scene
self.controller = controller
self._name = self.vera_scene.name
# Append device id to prevent name clashes in HA.
self.vera_id = VERA_ID_FORMAT.format(
slugify(vera_scene.name), vera_scene.scene_id)
def update(self):
"""Update the scene status."""
self.vera_scene.refresh()
def activate(self, **kwargs):
"""Activate the scene."""
self.vera_scene.activate()
@property
def name(self):
"""Return the name of the scene."""
return self._name
@property
def device_state_attributes(self):
"""Return the state attributes of the scene."""
return {'vera_scene_id': self.vera_scene.vera_scene_id}
@property
def should_poll(self):
"""Return that polling is not necessary."""
return False
+103
View File
@@ -0,0 +1,103 @@
"""
Support for ADS sensors.
For more details about this platform, please refer to the documentation.
https://home-assistant.io/components/sensor.ads/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components import ads
from homeassistant.components.ads import CONF_ADS_VAR, CONF_ADS_TYPE, \
CONF_ADS_FACTOR
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS sensor'
DEPENDENCIES = ['ads']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=''): cv.string,
vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT): vol.In(
[ads.ADSTYPE_INT, ads.ADSTYPE_UINT, ads.ADSTYPE_BYTE]
),
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an ADS sensor device."""
ads_hub = hass.data.get(ads.DATA_ADS)
ads_var = config.get(CONF_ADS_VAR)
ads_type = config.get(CONF_ADS_TYPE)
name = config.get(CONF_NAME)
unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
factor = config.get(CONF_ADS_FACTOR)
entity = AdsSensor(ads_hub, ads_var, ads_type, name,
unit_of_measurement, factor)
add_devices([entity])
class AdsSensor(Entity):
"""Representation of an ADS sensor entity."""
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement,
factor):
"""Initialize AdsSensor entity."""
self._ads_hub = ads_hub
self._name = name
self._value = None
self._unit_of_measurement = unit_of_measurement
self.ads_var = ads_var
self.ads_type = ads_type
self.factor = factor
@asyncio.coroutine
def async_added_to_hass(self):
"""Register device notification."""
def update(name, value):
"""Handle device notifications."""
_LOGGER.debug('Variable %s changed its value to %d', name, value)
# if factor is set use it otherwise not
if self.factor is None:
self._value = value
else:
self._value = value / self.factor
self.schedule_update_ha_state()
self.hass.async_add_job(
self._ads_hub.add_device_notification,
self.ads_var, self._ads_hub.ADS_TYPEMAP[self.ads_type], update
)
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._value
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def should_poll(self):
"""Return False because entity pushes its state to HA."""
return False
@@ -7,25 +7,21 @@ https://home-assistant.io/components/sensor.alarmdecoder/
import asyncio
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE)
from homeassistant.const import (STATE_UNKNOWN)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up for AlarmDecoder sensor devices."""
_LOGGER.debug("AlarmDecoderSensor: async_setup_platform")
_LOGGER.debug("AlarmDecoderSensor: setup_platform")
device = AlarmDecoderSensor(hass)
async_add_devices([device])
add_devices([device])
class AlarmDecoderSensor(Entity):
@@ -34,23 +30,20 @@ class AlarmDecoderSensor(Entity):
def __init__(self, hass):
"""Initialize the alarm panel."""
self._display = ""
self._state = STATE_UNKNOWN
self._state = None
self._icon = 'mdi:alarm-check'
self._name = 'Alarm Panel Display'
_LOGGER.debug("Setting up panel")
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_PANEL_MESSAGE, self._message_callback)
@callback
def _message_callback(self, message):
if self._display != message.text:
self._display = message.text
self.async_schedule_update_ha_state()
self.schedule_update_ha_state()
@property
def icon(self):
@@ -0,0 +1,110 @@
"""
Stock market information from Alpha Vantage.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.alpha_vantage/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['alpha_vantage==1.3.6']
_LOGGER = logging.getLogger(__name__)
ATTR_CLOSE = 'close'
ATTR_HIGH = 'high'
ATTR_LOW = 'low'
ATTR_VOLUME = 'volume'
CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage."
CONF_SYMBOLS = 'symbols'
DEFAULT_SYMBOL = 'GOOGL'
ICON = 'mdi:currency-usd'
SCAN_INTERVAL = timedelta(minutes=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_SYMBOLS, default=[DEFAULT_SYMBOL]):
vol.All(cv.ensure_list, [cv.string]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Alpha Vantage sensor."""
from alpha_vantage.timeseries import TimeSeries
api_key = config.get(CONF_API_KEY)
symbols = config.get(CONF_SYMBOLS)
timeseries = TimeSeries(key=api_key)
dev = []
for symbol in symbols:
try:
timeseries.get_intraday(symbol)
except ValueError:
_LOGGER.error(
"API Key is not valid or symbol '%s' not known", symbol)
return
dev.append(AlphaVantageSensor(timeseries, symbol))
add_devices(dev, True)
class AlphaVantageSensor(Entity):
"""Representation of a Alpha Vantage sensor."""
def __init__(self, timeseries, symbol):
"""Initialize the sensor."""
self._name = symbol
self._timeseries = timeseries
self._symbol = symbol
self.values = None
self._unit_of_measurement = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._symbol
@property
def state(self):
"""Return the state of the sensor."""
return self.values['1. open']
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self.values is not None:
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
ATTR_CLOSE: self.values['4. close'],
ATTR_HIGH: self.values['2. high'],
ATTR_LOW: self.values['3. low'],
ATTR_VOLUME: self.values['5. volume'],
}
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return ICON
def update(self):
"""Get the latest data and updates the states."""
all_values, _ = self._timeseries.get_intraday(self._symbol)
self.values = next(iter(all_values.values()))
+85
View File
@@ -0,0 +1,85 @@
"""
Support for Canary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.canary/
"""
from homeassistant.components.canary import DATA_CANARY
from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
DEPENDENCIES = ['canary']
SENSOR_VALUE_PRECISION = 1
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Canary sensors."""
data = hass.data[DATA_CANARY]
devices = []
from canary.api import SensorType
for location in data.locations:
for device in location.devices:
if device.is_online:
for sensor_type in SensorType:
devices.append(CanarySensor(data, sensor_type, location,
device))
add_devices(devices, True)
class CanarySensor(Entity):
"""Representation of a Canary sensor."""
def __init__(self, data, sensor_type, location, device):
"""Initialize the sensor."""
self._data = data
self._sensor_type = sensor_type
self._device_id = device.device_id
self._is_celsius = location.is_celsius
self._sensor_value = None
sensor_type_name = sensor_type.value.replace("_", " ").title()
self._name = '{} {} {}'.format(location.name,
device.name,
sensor_type_name)
@property
def name(self):
"""Return the name of the Canary sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._sensor_value
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
return "sensor_canary_{}_{}".format(self._device_id,
self._sensor_type.value)
@property
def unit_of_measurement(self):
"""Return the unit of measurement this sensor expresses itself in."""
from canary.api import SensorType
if self._sensor_type == SensorType.TEMPERATURE:
return TEMP_CELSIUS if self._is_celsius else TEMP_FAHRENHEIT
elif self._sensor_type == SensorType.HUMIDITY:
return "%"
elif self._sensor_type == SensorType.AIR_QUALITY:
return ""
return None
def update(self):
"""Get the latest state of the sensor."""
self._data.update()
readings = self._data.get_readings(self._device_id)
value = next((
reading.value for reading in readings
if reading.sensor_type == self._sensor_type), None)
if value is not None:
self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION)
+2 -1
View File
@@ -31,6 +31,7 @@ CONF_COST = 'cost'
CONF_CURRENT_VALUES = 'current_values'
DEFAULT_PERIOD = 'year'
DEFAULT_UTC_OFFSET = '0'
SENSOR_TYPES = {
CONF_INSTANT: ['Energy Usage', 'W'],
@@ -50,7 +51,7 @@ SENSORS_SCHEMA = vol.Schema({
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_APPTOKEN): cv.string,
vol.Optional(CONF_UTC_OFFSET): cv.string,
vol.Optional(CONF_UTC_OFFSET, default=DEFAULT_UTC_OFFSET): cv.string,
vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA]
})
@@ -30,7 +30,7 @@ UNIT_OF_MEASUREMENT = 'W'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_CHANNEL_ID): cv.positive_int,
vol.Required(CONF_CHANNEL_ID): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
+127
View File
@@ -0,0 +1,127 @@
"""
Parse prices of a item from gearbest.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.gearbest/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
from homeassistant.const import (CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY)
REQUIREMENTS = ['gearbest_parser==1.0.5']
_LOGGER = logging.getLogger(__name__)
CONF_ITEMS = 'items'
ICON = 'mdi:coin'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=2*60*60) # 2h
MIN_TIME_BETWEEN_CURRENCY_UPDATES = timedelta(seconds=12*60*60) # 12h
_ITEM_SCHEMA = vol.All(
vol.Schema({
vol.Exclusive(CONF_URL, 'XOR'): cv.string,
vol.Exclusive(CONF_ID, 'XOR'): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_CURRENCY): cv.string
}), cv.has_at_least_one_key(CONF_URL, CONF_ID)
)
_ITEMS_SCHEMA = vol.Schema([_ITEM_SCHEMA])
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ITEMS): _ITEMS_SCHEMA,
vol.Required(CONF_CURRENCY): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Gearbest sensor."""
from gearbest_parser import CurrencyConverter
currency = config.get(CONF_CURRENCY)
sensors = []
items = config.get(CONF_ITEMS)
converter = CurrencyConverter()
converter.update()
for item in items:
try:
sensors.append(GearbestSensor(converter, item, currency))
except ValueError as exc:
_LOGGER.error(exc)
def currency_update(event_time):
"""Update currency list."""
converter.update()
track_time_interval(hass,
currency_update,
MIN_TIME_BETWEEN_CURRENCY_UPDATES)
add_devices(sensors, True)
class GearbestSensor(Entity):
"""Implementation of the sensor."""
def __init__(self, converter, item, currency):
"""Initialize the sensor."""
from gearbest_parser import GearbestParser
self._name = item.get(CONF_NAME)
self._parser = GearbestParser()
self._parser.set_currency_converter(converter)
self._item = self._parser.load(item.get(CONF_ID),
item.get(CONF_URL),
item.get(CONF_CURRENCY, currency))
if self._item is None:
raise ValueError("id and url could not be resolved")
@property
def name(self):
"""Return the name of the item."""
return self._name if self._name is not None else self._item.name
@property
def icon(self):
"""Return the icon for the frontend."""
return ICON
@property
def state(self):
"""Return the price of the selected product."""
return self._item.price
@property
def unit_of_measurement(self):
"""Return the currency."""
return self._item.currency
@property
def entity_picture(self):
"""Return the image."""
return self._item.image
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {'name': self._item.name,
'description': self._item.description,
'currency': self._item.currency,
'url': self._item.url}
return attrs
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest price from gearbest and updates the state."""
self._item.update()
@@ -282,6 +282,9 @@ class ISYSensorDevice(isy.ISYDevice):
@property
def state(self) -> str:
"""Get the state of the ISY994 sensor device."""
if self.is_unknown():
return None
if len(self._node.uom) == 1:
if self._node.uom[0] in UOM_TO_STATES:
states = UOM_TO_STATES.get(self._node.uom[0])
+66 -55
View File
@@ -5,85 +5,94 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.luftdaten/
"""
import asyncio
import json
import logging
from datetime import timedelta
import logging
import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_NAME, CONF_RESOURCE, CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS,
TEMP_CELSIUS)
from homeassistant.helpers.entity import Entity
ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['luftdaten==0.1.1']
_LOGGER = logging.getLogger(__name__)
ATTR_SENSOR_ID = 'sensor_id'
CONF_ATTRIBUTION = "Data provided by luftdaten.info"
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
SENSOR_TEMPERATURE = 'temperature'
SENSOR_HUMIDITY = 'humidity'
SENSOR_PM10 = 'P1'
SENSOR_PM2_5 = 'P2'
SENSOR_PRESSURE = 'pressure'
SENSOR_TYPES = {
SENSOR_TEMPERATURE: ['Temperature', TEMP_CELSIUS],
SENSOR_HUMIDITY: ['Humidity', '%'],
SENSOR_PRESSURE: ['Pressure', 'Pa'],
SENSOR_PM10: ['PM10', VOLUME_MICROGRAMS_PER_CUBIC_METER],
SENSOR_PM2_5: ['PM2.5', VOLUME_MICROGRAMS_PER_CUBIC_METER]
}
DEFAULT_NAME = 'Luftdaten Sensor'
DEFAULT_RESOURCE = 'https://api.luftdaten.info/v1/sensor/'
DEFAULT_VERIFY_SSL = True
DEFAULT_NAME = 'Luftdaten'
CONF_SENSORID = 'sensorid'
SCAN_INTERVAL = timedelta(minutes=3)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORID): cv.positive_int,
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_RESOURCE, default=DEFAULT_RESOURCE): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Luftdaten sensor."""
from luftdaten import Luftdaten
name = config.get(CONF_NAME)
sensorid = config.get(CONF_SENSORID)
verify_ssl = config.get(CONF_VERIFY_SSL)
sensor_id = config.get(CONF_SENSORID)
resource = '{}{}/'.format(config.get(CONF_RESOURCE), sensorid)
session = async_get_clientsession(hass)
luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session))
rest_client = LuftdatenData(resource, verify_ssl)
rest_client.update()
yield from luftdaten.async_update()
if rest_client.data is None:
_LOGGER.error("Unable to fetch Luftdaten data")
return False
if luftdaten.data is None:
_LOGGER.error("Sensor is not available: %s", sensor_id)
return
devices = []
for variable in config[CONF_MONITORED_CONDITIONS]:
devices.append(LuftdatenSensor(rest_client, name, variable))
if luftdaten.data.values[variable] is None:
_LOGGER.warning("It might be that sensor %s is not providing "
"measurements for %s", sensor_id, variable)
devices.append(LuftdatenSensor(luftdaten, name, variable, sensor_id))
async_add_devices(devices, True)
async_add_devices(devices)
class LuftdatenSensor(Entity):
"""Implementation of a LuftdatenSensor sensor."""
"""Implementation of a Luftdaten sensor."""
def __init__(self, rest_client, name, sensor_type):
"""Initialize the LuftdatenSensor sensor."""
self.rest_client = rest_client
def __init__(self, luftdaten, name, sensor_type, sensor_id):
"""Initialize the Luftdaten sensor."""
self.luftdaten = luftdaten
self._name = name
self._state = None
self._sensor_id = sensor_id
self.sensor_type = sensor_type
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@@ -95,48 +104,50 @@ class LuftdatenSensor(Entity):
@property
def state(self):
"""Return the state of the device."""
return self._state
return self.luftdaten.data.values[self.sensor_type]
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
def update(self):
"""Get the latest data from REST API and update the state."""
self.rest_client.update()
value = self.rest_client.data
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self.luftdaten.data.meta is None:
return
if value is None:
self._state = None
else:
parsed_json = json.loads(value)
attr = {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
ATTR_SENSOR_ID: self._sensor_id,
'lat': self.luftdaten.data.meta['latitude'],
'long': self.luftdaten.data.meta['longitude'],
}
return attr
log_entries_count = len(parsed_json) - 1
latest_log_entry = parsed_json[log_entries_count]
sensordata_values = latest_log_entry['sensordatavalues']
for sensordata_value in sensordata_values:
if sensordata_value['value_type'] == self.sensor_type:
self._state = sensordata_value['value']
@asyncio.coroutine
def async_update(self):
"""Get the latest data from luftdaten.info and update the state."""
try:
yield from self.luftdaten.async_update()
except TypeError:
pass
class LuftdatenData(object):
"""Class for handling the data retrieval."""
def __init__(self, resource, verify_ssl):
def __init__(self, data):
"""Initialize the data object."""
self._request = requests.Request('GET', resource).prepare()
self._verify_ssl = verify_ssl
self.data = None
self.data = data
@Throttle(MIN_TIME_BETWEEN_UPDATES)
@asyncio.coroutine
def async_update(self):
"""Get the latest data from luftdaten.info."""
from luftdaten.exceptions import LuftdatenError
def update(self):
"""Get the latest data from Luftdaten service."""
try:
with requests.Session() as sess:
response = sess.send(
self._request, timeout=10, verify=self._verify_ssl)
self.data = response.text
except requests.exceptions.RequestException:
_LOGGER.error("Error fetching data: %s", self._request)
self.data = None
yield from self.data.async_get_data()
except LuftdatenError:
_LOGGER.error("Unable to retrieve data from luftdaten.info")
+2 -2
View File
@@ -12,7 +12,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC)
CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC
)
REQUIREMENTS = ['miflora==0.1.16']
@@ -20,7 +21,6 @@ _LOGGER = logging.getLogger(__name__)
CONF_ADAPTER = 'adapter'
CONF_CACHE = 'cache_value'
CONF_FORCE_UPDATE = 'force_update'
CONF_MEDIAN = 'median'
CONF_RETRIES = 'retries'
CONF_TIMEOUT = 'timeout'
+2 -2
View File
@@ -13,7 +13,8 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS
from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT)
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN,
CONF_UNIT_OF_MEASUREMENT)
from homeassistant.helpers.entity import Entity
import homeassistant.components.mqtt as mqtt
import homeassistant.helpers.config_validation as cv
@@ -22,7 +23,6 @@ from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
CONF_FORCE_UPDATE = 'force_update'
CONF_EXPIRE_AFTER = 'expire_after'
DEFAULT_NAME = 'MQTT Sensor'
@@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME)
monitored_conditions = config.get(CONF_MONITORED_CONDITIONS)
tools = octoprint_api.get_tools()
_LOGGER.error(str(tools))
if "Temperatures" in monitored_conditions:
if not tools:
+45 -6
View File
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.rest/
"""
import logging
import json
import voluptuous as vol
import requests
@@ -12,10 +13,11 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, CONF_VERIFY_SSL, CONF_USERNAME,
CONF_PASSWORD, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION, CONF_HEADERS)
CONF_AUTHENTICATION, CONF_FORCE_UPDATE, CONF_HEADERS, CONF_NAME,
CONF_METHOD, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE,
CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME,
CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, STATE_UNKNOWN)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
@@ -24,7 +26,9 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_METHOD = 'GET'
DEFAULT_NAME = 'REST Sensor'
DEFAULT_VERIFY_SSL = True
DEFAULT_FORCE_UPDATE = False
CONF_JSON_ATTRS = 'json_attributes'
METHODS = ['POST', 'GET']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -32,6 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_HEADERS): {cv.string: cv.string},
vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv,
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
@@ -40,6 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
})
@@ -55,6 +61,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
headers = config.get(CONF_HEADERS)
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
value_template = config.get(CONF_VALUE_TEMPLATE)
json_attrs = config.get(CONF_JSON_ATTRS)
force_update = config.get(CONF_FORCE_UPDATE)
if value_template is not None:
value_template.hass = hass
@@ -68,13 +77,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update()
add_devices([RestSensor(hass, rest, name, unit, value_template)], True)
add_devices([RestSensor(
hass, rest, name, unit, value_template, json_attrs, force_update
)], True)
class RestSensor(Entity):
"""Implementation of a REST sensor."""
def __init__(self, hass, rest, name, unit_of_measurement, value_template):
def __init__(self, hass, rest, name, unit_of_measurement,
value_template, json_attrs, force_update):
"""Initialize the REST sensor."""
self._hass = hass
self.rest = rest
@@ -82,6 +94,9 @@ class RestSensor(Entity):
self._state = STATE_UNKNOWN
self._unit_of_measurement = unit_of_measurement
self._value_template = value_template
self._json_attrs = json_attrs
self._attributes = None
self._force_update = force_update
@property
def name(self):
@@ -103,11 +118,30 @@ class RestSensor(Entity):
"""Return the state of the device."""
return self._state
@property
def force_update(self):
"""Force update."""
return self._force_update
def update(self):
"""Get the latest data from REST API and update the state."""
self.rest.update()
value = self.rest.data
if self._json_attrs:
self._attributes = {}
try:
json_dict = json.loads(value)
if isinstance(json_dict, dict):
attrs = {k: json_dict[k] for k in self._json_attrs
if k in json_dict}
self._attributes = attrs
else:
_LOGGER.warning("JSON result was not a dictionary")
except ValueError:
_LOGGER.warning("REST result could not be parsed as JSON")
_LOGGER.debug("Erroneous JSON: %s", value)
if value is None:
value = STATE_UNKNOWN
elif self._value_template is not None:
@@ -116,6 +150,11 @@ class RestSensor(Entity):
self._state = value
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._attributes
class RestData(object):
"""Class for handling the data retrieval."""
+4 -2
View File
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION)
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-ripple-api==0.0.2']
REQUIREMENTS = ['python-ripple-api==0.0.3']
CONF_ADDRESS = 'address'
CONF_ATTRIBUTION = "Data provided by ripple.com"
@@ -71,4 +71,6 @@ class RippleSensor(Entity):
def update(self):
"""Get the latest state of the sensor."""
from pyripple import get_balance
self._state = get_balance(self.address)
balance = get_balance(self.address)
if balance is not None:
self._state = balance
+1 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['shodan==1.7.5']
REQUIREMENTS = ['shodan==1.7.7']
_LOGGER = logging.getLogger(__name__)
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['psutil==5.4.1']
REQUIREMENTS = ['psutil==5.4.2']
_LOGGER = logging.getLogger(__name__)
+5 -4
View File
@@ -39,7 +39,7 @@ class TeslaSensor(TeslaDevice, Entity):
def __init__(self, tesla_device, controller, sensor_type=None):
"""Initialisation of the sensor."""
self.current_value = None
self._temperature_units = None
self._unit = None
self.last_changed_time = None
self.type = sensor_type
super().__init__(tesla_device, controller)
@@ -59,7 +59,7 @@ class TeslaSensor(TeslaDevice, Entity):
@property
def unit_of_measurement(self):
"""Return the unit_of_measurement of the device."""
return self._temperature_units
return self._unit
def update(self):
"""Update the state from the sensor."""
@@ -74,8 +74,9 @@ class TeslaSensor(TeslaDevice, Entity):
tesla_temp_units = self.tesla_device.measurement
if tesla_temp_units == 'F':
self._temperature_units = TEMP_FAHRENHEIT
self._unit = TEMP_FAHRENHEIT
else:
self._temperature_units = TEMP_CELSIUS
self._unit = TEMP_CELSIUS
else:
self.current_value = self.tesla_device.battery_level()
self._unit = "%"
+2 -2
View File
@@ -25,8 +25,8 @@ SCAN_INTERVAL = timedelta(seconds=5)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera controller devices."""
add_devices(
VeraSensor(device, VERA_CONTROLLER)
for device in VERA_DEVICES['sensor'])
VeraSensor(device, hass.data[VERA_CONTROLLER])
for device in hass.data[VERA_DEVICES]['sensor'])
class VeraSensor(VeraDevice, Entity):
+29 -4
View File
@@ -6,8 +6,10 @@ https://home-assistant.io/components/sensor.volvooncall/
"""
import logging
from math import floor
from homeassistant.components.volvooncall import VolvoEntity, RESOURCES
from homeassistant.components.volvooncall import (
VolvoEntity, RESOURCES, CONF_SCANDINAVIAN_MILES)
_LOGGER = logging.getLogger(__name__)
@@ -26,14 +28,37 @@ class VolvoSensor(VolvoEntity):
def state(self):
"""Return the state of the sensor."""
val = getattr(self.vehicle, self._attribute)
if val is None:
return val
if self._attribute == 'odometer':
return round(val / 1000) # km
return val
val /= 1000 # m -> km
if 'mil' in self.unit_of_measurement:
val /= 10 # km -> mil
if self._attribute == 'average_fuel_consumption':
val /= 10 # L/1000km -> L/100km
if 'mil' in self.unit_of_measurement:
return round(val, 2)
else:
return round(val, 1)
elif self._attribute == 'distance_to_empty':
return int(floor(val))
else:
return int(round(val))
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return RESOURCES[self._attribute][3]
unit = RESOURCES[self._attribute][3]
if self._state.config[CONF_SCANDINAVIAN_MILES] and 'km' in unit:
if self._attribute == 'average_fuel_consumption':
return 'L/mil'
else:
return unit.replace('km', 'mil')
return unit
@property
def icon(self):
+4 -53
View File
@@ -32,55 +32,6 @@ foursquare:
description: Vertical accuracy of the user's location, in meters.
example: 1
homematic:
virtualkey:
description: Press a virtual key from CCU/Homegear or simulate keypress.
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote.
example: BidCoS-RF
channel:
description: Channel for calling a keypress.
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
example: PRESS_LONG
proxy:
description: (Optional) for set a hosts value.
example: Hosts name from config
set_var_value:
description: Set the name of a node.
fields:
entity_id:
description: Name(s) of homematic central to set value.
example: 'homematic.ccu2'
name:
description: Name of the variable to set.
example: 'testvariable'
value:
description: New value
example: 1
set_dev_value:
description: Set a device property on RPC XML interface.
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote
example: BidCoS-RF
channel:
description: Channel for calling a keypress
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
example: PRESS_LONG
proxy:
description: (Optional) for set a hosts value
example: Hosts name from config
value:
description: New value
example: 1
reconnect:
description: Reconnect to all Homematic Hubs.
microsoft_face:
create_group:
description: Create a new person group.
@@ -437,7 +388,7 @@ input_text:
set_value:
description: Set the value of an input text entity.
fields:
entity_id:
entity_id:
description: Entity id of the input text to set the new value.
example: 'input_text.text1'
value:
@@ -448,7 +399,7 @@ input_number:
set_value:
description: Set the value of an input number entity.
fields:
entity_id:
entity_id:
description: Entity id of the input number to set the new value.
example: 'input_number.threshold'
value:
@@ -457,13 +408,13 @@ input_number:
increment:
description: Increment the value of an input number entity by its stepping.
fields:
entity_id:
entity_id:
description: Entity id of the input number the should be incremented.
example: 'input_number.threshold'
decrement:
description: Decrement the value of an input number entity by its stepping.
fields:
entity_id:
entity_id:
description: Entity id of the input number the should be decremented.
example: 'input_number.threshold'
+9 -4
View File
@@ -15,7 +15,7 @@ DEPENDENCIES = ['mqtt']
CONF_INTENTS = 'intents'
CONF_ACTION = 'action'
INTENT_TOPIC = 'hermes/nlu/intentParsed'
INTENT_TOPIC = 'hermes/intent/#'
_LOGGER = logging.getLogger(__name__)
@@ -32,7 +32,8 @@ INTENT_SCHEMA = vol.Schema({
vol.Required('slotName'): str,
vol.Required('value'): {
vol.Required('kind'): str,
vol.Required('value'): cv.match_all
vol.Optional('value'): cv.match_all,
vol.Optional('rawValue'): cv.match_all
}
}]
}, extra=vol.ALLOW_EXTRA)
@@ -59,8 +60,12 @@ def async_setup(hass, config):
return
intent_type = request['intent']['intentName'].split('__')[-1]
slots = {slot['slotName']: {'value': slot['value']['value']}
for slot in request.get('slots', [])}
slots = {}
for slot in request.get('slots', []):
if 'value' in slot['value']:
slots[slot['slotName']] = {'value': slot['value']['value']}
else:
slots[slot['slotName']] = {'value': slot['rawValue']}
try:
yield from intent.async_handle(
+85
View File
@@ -0,0 +1,85 @@
"""
Support for ADS switch platform.
For more details about this platform, please refer to the documentation.
https://home-assistant.io/components/switch.ads/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR
from homeassistant.helpers.entity import ToggleEntity
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['ads']
DEFAULT_NAME = 'ADS Switch'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up switch platform for ADS."""
ads_hub = hass.data.get(DATA_ADS)
name = config.get(CONF_NAME)
ads_var = config.get(CONF_ADS_VAR)
add_devices([AdsSwitch(ads_hub, name, ads_var)], True)
class AdsSwitch(ToggleEntity):
"""Representation of an Ads switch device."""
def __init__(self, ads_hub, name, ads_var):
"""Initialize the AdsSwitch entity."""
self._ads_hub = ads_hub
self._on_state = False
self._name = name
self.ads_var = ads_var
@asyncio.coroutine
def async_added_to_hass(self):
"""Register device notification."""
def update(name, value):
"""Handle device notification."""
_LOGGER.debug('Variable %s changed its value to %d',
name, value)
self._on_state = value
self.schedule_update_ha_state()
self.hass.async_add_job(
self._ads_hub.add_device_notification,
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update
)
@property
def is_on(self):
"""Return if the switch is turned on."""
return self._on_state
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def should_poll(self):
"""Return False because entity pushes its state to HA."""
return False
def turn_on(self, **kwargs):
"""Turn the switch on."""
self._ads_hub.write_by_name(self.ads_var, True,
self._ads_hub.PLCTYPE_BOOL)
def turn_off(self, **kwargs):
"""Turn the switch off."""
self._ads_hub.write_by_name(self.ads_var, False,
self._ads_hub.PLCTYPE_BOOL)
+4 -1
View File
@@ -69,7 +69,10 @@ class ISYSwitchDevice(isy.ISYDevice, SwitchDevice):
@property
def state(self) -> str:
"""Get the state of the ISY994 device."""
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
if self.is_unknown():
return None
else:
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
def turn_off(self, **kwargs) -> None:
"""Send the turn on command to the ISY994 switch."""
+8 -5
View File
@@ -60,18 +60,21 @@ class MochadSwitch(SwitchDevice):
def turn_on(self, **kwargs):
"""Turn the switch on."""
self._state = True
self.device.send_cmd('on')
self._controller.read_data()
with mochad.REQ_LOCK:
self.device.send_cmd('on')
self._controller.read_data()
def turn_off(self, **kwargs):
"""Turn the switch off."""
self._state = False
self.device.send_cmd('off')
self._controller.read_data()
with mochad.REQ_LOCK:
self.device.send_cmd('off')
self._controller.read_data()
def _get_device_status(self):
"""Get the status of the switch from mochad."""
status = self.device.get_status().rstrip()
with mochad.REQ_LOCK:
status = self.device.get_status().rstrip()
return status == 'on'
@property
+19 -5
View File
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.tplink/
"""
import logging
import time
import voluptuous as vol
@@ -23,9 +22,12 @@ ATTR_TOTAL_CONSUMPTION = 'total_consumption'
ATTR_DAILY_CONSUMPTION = 'daily_consumption'
ATTR_CURRENT = 'current'
CONF_LEDS = 'enable_leds'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_LEDS, default=True): cv.boolean,
})
@@ -35,18 +37,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
from pyHS100 import SmartPlug
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
leds_on = config.get(CONF_LEDS)
add_devices([SmartPlugSwitch(SmartPlug(host), name)], True)
add_devices([SmartPlugSwitch(SmartPlug(host), name, leds_on)], True)
class SmartPlugSwitch(SwitchDevice):
"""Representation of a TPLink Smart Plug switch."""
def __init__(self, smartplug, name):
def __init__(self, smartplug, name, leds_on):
"""Initialize the switch."""
self.smartplug = smartplug
self._name = name
self._leds_on = leds_on
self._state = None
self._available = True
# Set up emeter cache
self._emeter_params = {}
@@ -55,6 +60,11 @@ class SmartPlugSwitch(SwitchDevice):
"""Return the name of the Smart Plug, if any."""
return self._name
@property
def available(self) -> bool:
"""Return if switch is available."""
return self._available
@property
def is_on(self):
"""Return true if switch is on."""
@@ -77,12 +87,15 @@ class SmartPlugSwitch(SwitchDevice):
"""Update the TP-Link switch's state."""
from pyHS100 import SmartDeviceException
try:
self._available = True
self._state = self.smartplug.state == \
self.smartplug.SWITCH_STATE_ON
if self._name is None:
self._name = self.smartplug.alias
self.smartplug.led = self._leds_on
if self.smartplug.has_emeter:
emeter_readings = self.smartplug.get_emeter_realtime()
@@ -100,8 +113,9 @@ class SmartPlugSwitch(SwitchDevice):
self._emeter_params[ATTR_DAILY_CONSUMPTION] \
= "%.2f kW" % emeter_statics[int(time.strftime("%e"))]
except KeyError:
# device returned no daily history
# Device returned no daily history
pass
except (SmartDeviceException, OSError) as ex:
_LOGGER.warning('Could not read state for %s: %s', self.name, ex)
_LOGGER.warning("Could not read state for %s: %s", self.name, ex)
self._available = False
+2 -2
View File
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera switches."""
add_devices(
VeraSwitch(device, VERA_CONTROLLER) for
device in VERA_DEVICES['switch'])
VeraSwitch(device, hass.data[VERA_CONTROLLER]) for
device in hass.data[VERA_DEVICES]['switch'])
class VeraSwitch(VeraDevice, SwitchDevice):
+1 -1
View File
@@ -24,7 +24,7 @@ APPLICATION_NAME = 'Home Assistant'
DOMAIN = 'tellduslive'
REQUIREMENTS = ['tellduslive==0.10.3']
REQUIREMENTS = ['tellduslive==0.10.4']
_LOGGER = logging.getLogger(__name__)

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