Merge pull request #5530 from home-assistant/dev

0.37
This commit is contained in:
Paulus Schoutsen 2017-01-28 15:13:43 -08:00 committed by GitHub
commit 46aa2e7ce1
241 changed files with 8791 additions and 2355 deletions

View File

@ -46,6 +46,9 @@ omit =
homeassistant/components/isy994.py homeassistant/components/isy994.py
homeassistant/components/*/isy994.py homeassistant/components/*/isy994.py
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
homeassistant/components/modbus.py homeassistant/components/modbus.py
homeassistant/components/*/modbus.py homeassistant/components/*/modbus.py
@ -122,6 +125,9 @@ omit =
homeassistant/components/mochad.py homeassistant/components/mochad.py
homeassistant/components/*/mochad.py homeassistant/components/*/mochad.py
homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
@ -130,6 +136,7 @@ omit =
homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/concord232.py
homeassistant/components/binary_sensor/flic.py homeassistant/components/binary_sensor/flic.py
homeassistant/components/binary_sensor/hikvision.py homeassistant/components/binary_sensor/hikvision.py
homeassistant/components/binary_sensor/iss.py
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py homeassistant/components/browser.py
homeassistant/components/camera/amcrest.py homeassistant/components/camera/amcrest.py
@ -160,14 +167,17 @@ omit =
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/linksys_ap.py
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ping.py homeassistant/components/device_tracker/ping.py
homeassistant/components/device_tracker/sky_hub.py
homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py homeassistant/components/device_tracker/swisscom.py
homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tado.py
homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/trackr.py homeassistant/components/device_tracker/trackr.py
homeassistant/components/device_tracker/ubus.py homeassistant/components/device_tracker/ubus.py
@ -185,7 +195,9 @@ omit =
homeassistant/components/joaoapps_join.py homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py homeassistant/components/keyboard_remote.py
homeassistant/components/light/avion.py
homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinksticklight.py
homeassistant/components/light/decora.py
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
@ -195,8 +207,10 @@ omit =
homeassistant/components/light/tikteck.py homeassistant/components/light/tikteck.py
homeassistant/components/light/x10.py homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py homeassistant/components/light/yeelight.py
homeassistant/components/light/piglow.py
homeassistant/components/light/zengge.py homeassistant/components/light/zengge.py
homeassistant/components/lirc.py homeassistant/components/lirc.py
homeassistant/components/media_player/anthemav.py
homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
@ -208,6 +222,7 @@ omit =
homeassistant/components/media_player/emby.py homeassistant/components/media_player/emby.py
homeassistant/components/media_player/firetv.py homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/hdmi_cec.py
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/lg_netcast.py
@ -231,6 +246,7 @@ omit =
homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/discord.py
homeassistant/components/notify/facebook.py homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
@ -256,12 +272,13 @@ omit =
homeassistant/components/notify/telegram.py homeassistant/components/notify/telegram.py
homeassistant/components/notify/telstra.py homeassistant/components/notify/telstra.py
homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twilio_call.py
homeassistant/components/notify/twitter.py homeassistant/components/notify/twitter.py
homeassistant/components/notify/xmpp.py homeassistant/components/notify/xmpp.py
homeassistant/components/nuimo_controller.py homeassistant/components/nuimo_controller.py
homeassistant/components/openalpr.py
homeassistant/components/remote/harmony.py homeassistant/components/remote/harmony.py
homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/sensor/amcrest.py
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py homeassistant/components/sensor/bbox.py
@ -293,7 +310,6 @@ omit =
homeassistant/components/sensor/hddtemp.py homeassistant/components/sensor/hddtemp.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/hydroquebec.py homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/iss.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py
@ -318,6 +334,7 @@ omit =
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/skybeacon.py
homeassistant/components/sensor/sma.py homeassistant/components/sensor/sma.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/sonarr.py
@ -348,6 +365,7 @@ omit =
homeassistant/components/switch/digitalloggers.py homeassistant/components/switch/digitalloggers.py
homeassistant/components/switch/dlink.py homeassistant/components/switch/dlink.py
homeassistant/components/switch/edimax.py homeassistant/components/switch/edimax.py
homeassistant/components/switch/hdmi_cec.py
homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/hook.py homeassistant/components/switch/hook.py
homeassistant/components/switch/kankun.py homeassistant/components/switch/kankun.py
@ -362,6 +380,7 @@ omit =
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py homeassistant/components/switch/wake_on_lan.py
homeassistant/components/thingspeak.py homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py
homeassistant/components/tts/picotts.py homeassistant/components/tts/picotts.py
homeassistant/components/upnp.py homeassistant/components/upnp.py
homeassistant/components/weather/bom.py homeassistant/components/weather/bom.py

6
.ignore Normal file
View File

@ -0,0 +1,6 @@
# Patterns matched in this file will be ignored by supported search utilities
# Ignore generated html and javascript files
/homeassistant/components/frontend/www_static/*.html
/homeassistant/components/frontend/www_static/*.js
/homeassistant/components/frontend/www_static/panels/*.html

View File

@ -8,15 +8,16 @@ matrix:
env: TOXENV=requirements env: TOXENV=requirements
- python: "3.4.2" - python: "3.4.2"
env: TOXENV=lint env: TOXENV=lint
- python: "3.5" # - python: "3.5"
env: TOXENV=typing # env: TOXENV=typing
- python: "3.5" - python: "3.5"
env: TOXENV=py35 env: TOXENV=py35
- python: "3.6" - python: "3.6"
env: TOXENV=py36 env: TOXENV=py36
allow_failures: # allow_failures:
- python: "3.5" # - python: "3.5"
env: TOXENV=typing # env: TOXENV=typing
cache: cache:
directories: directories:
- $HOME/.cache/pip - $HOME/.cache/pip

39
CLA.md Normal file
View File

@ -0,0 +1,39 @@
# Contributor License Agreement
```
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the Apache 2.0 license; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the Apache 2.0 license; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it) is maintained indefinitely
and may be redistributed consistent with this project or the open
source license(s) involved.
```
## Attribution
The text of this license is available under the [Creative Commons Attribution-ShareAlike 3.0 Unported License](http://creativecommons.org/licenses/by-sa/3.0/). It is based on the Linux [Developer Certificate Of Origin](http://elinux.org/Developer_Certificate_Of_Origin), but is modified to explicitly use the Apache 2.0 license
and not mention sign-off.
## Signing
To sign this CLA you must first submit a pull request to a repository under the Home Assistant organization.
## Adoption
This Contributor License Agreement (CLA) was first announced on January 21st, 2017 in [this][cla-blog] blog post and adopted January 28th, 2017.
[cla-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/

80
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,80 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [safety@home-assistant.io][email]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available [here][version].
## Adoption
This Code of Conduct was first adopted January 21st, 2017 and announced in [this][coc-blog] blog post.
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
[email]: mailto:safety@home-assistant.io
[coc-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/

View File

@ -7,7 +7,7 @@ RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
# Copy build scripts # Copy build scripts
COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/ COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/install_phantomjs script/
RUN script/setup_docker_prereqs RUN script/setup_docker_prereqs
# Install hass component dependencies # Install hass component dependencies

20
LICENSE
View File

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Paulus Schoutsen
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

194
LICENSE.md Normal file
View File

@ -0,0 +1,194 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -87,7 +87,7 @@ components <https://home-assistant.io/developers/creating_components/>`__.
If you run into issues while using Home Assistant or during development If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help of a component, check the `Home Assistant help
section <https://home-assistant.io/help/>`__ how to reach us. section <https://home-assistant.io/help/>`__ of our website for further help and information.
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master .. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master
:target: https://travis-ci.org/home-assistant/home-assistant :target: https://travis-ci.org/home-assistant/home-assistant

View File

@ -508,8 +508,10 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
Async friendly. Async friendly.
""" """
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)s") "[%(name)s] %(message)s")
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
datefmt = '%y-%m-%d %H:%M:%S'
# suppress overly verbose logs from libraries that aren't helpful # suppress overly verbose logs from libraries that aren't helpful
logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("requests").setLevel(logging.WARNING)
@ -519,8 +521,8 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
try: try:
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter( logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
fmt, colorfmt,
datefmt='%y-%m-%d %H:%M:%S', datefmt=datefmt,
reset=True, reset=True,
log_colors={ log_colors={
'DEBUG': 'cyan', 'DEBUG': 'cyan',
@ -554,9 +556,7 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
err_log_path, mode='w', delay=True) err_log_path, mode='w', delay=True)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter( err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%y-%m-%d %H:%M:%S'))
async_handler = AsyncHandler(hass.loop, err_handler) async_handler = AsyncHandler(hass.loop, err_handler)
hass.data[core.DATA_ASYNCHANDLER] = async_handler hass.data[core.DATA_ASYNCHANDLER] = async_handler

View File

@ -0,0 +1,68 @@
"""
Interfaces with Wink Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.wink/
"""
import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (STATE_UNKNOWN,
STATE_ALARM_DISARMED,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY)
from homeassistant.components.wink import WinkDevice
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['wink']
STATE_ALARM_PRIVACY = 'Private'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink platform."""
import pywink
for camera in pywink.get_cameras():
add_devices([WinkCameraDevice(camera, hass)])
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
"""Representation a Wink camera alarm."""
def __init__(self, wink, hass):
"""Initialize the Wink alarm."""
WinkDevice.__init__(self, wink, hass)
@property
def state(self):
"""Return the state of the device."""
wink_state = self.wink.state()
if wink_state == "away":
state = STATE_ALARM_ARMED_AWAY
elif wink_state == "home":
state = STATE_ALARM_DISARMED
elif wink_state == "night":
state = STATE_ALARM_ARMED_HOME
else:
state = STATE_UNKNOWN
return state
def alarm_disarm(self, code=None):
"""Send disarm command."""
self.wink.set_mode("home")
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self.wink.set_mode("night")
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self.wink.set_mode("away")
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'private': self.wink.private()
}

View File

@ -63,7 +63,7 @@ def read_input(pin):
"""Read a value from a GPIO.""" """Read a value from a GPIO."""
# pylint: disable=import-error,undefined-variable # pylint: disable=import-error,undefined-variable
import Adafruit_BBIO.GPIO as GPIO import Adafruit_BBIO.GPIO as GPIO
return GPIO.input(pin) return GPIO.input(pin) is GPIO.HIGH
def edge_detect(pin, event_callback, bounce): def edge_detect(pin, event_callback, bounce):

View File

@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the aREST binary sensor.""" """Set up the aREST binary sensor."""
resource = config.get(CONF_RESOURCE) resource = config.get(CONF_RESOURCE)
pin = config.get(CONF_PIN) pin = config.get(CONF_PIN)
sensor_class = config.get(CONF_SENSOR_CLASS) sensor_class = config.get(CONF_SENSOR_CLASS)
@ -38,13 +38,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try: try:
response = requests.get(resource, timeout=10).json() response = requests.get(resource, timeout=10).json()
except requests.exceptions.MissingSchema: except requests.exceptions.MissingSchema:
_LOGGER.error('Missing resource or schema in configuration. ' _LOGGER.error("Missing resource or schema in configuration. "
'Add http:// to your URL.') "Add http:// to your URL")
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error('No route to device at %s. ' _LOGGER.error("No route to device at %s", resource)
'Please check the IP address in the configuration file.',
resource)
return False return False
arest = ArestData(resource, pin) arest = ArestData(resource, pin)
@ -67,10 +65,10 @@ class ArestBinarySensor(BinarySensorDevice):
self.update() self.update()
if self._pin is not None: if self._pin is not None:
request = requests.get('{}/mode/{}/i'.format request = requests.get(
(self._resource, self._pin), timeout=10) '{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
if request.status_code is not 200: if request.status_code is not 200:
_LOGGER.error("Can't set mode. Is device offline?") _LOGGER.error("Can't set mode of %s", self._resource)
@property @property
def name(self): def name(self):
@ -109,5 +107,4 @@ class ArestData(object):
self._resource, self._pin), timeout=10) self._resource, self._pin), timeout=10)
self.data = {'state': response.json()['return_value']} self.data = {'state': response.json()['return_value']}
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device '%s'. Is device offline?", _LOGGER.error("No route to device '%s'", self._resource)
self._resource)

View File

@ -0,0 +1,89 @@
"""
Support for binary sensor using Beaglebone Black GPIO.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.bbb_gpio/
"""
import logging
import voluptuous as vol
import homeassistant.components.bbb_gpio as bbb_gpio
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['bbb_gpio']
CONF_PINS = 'pins'
CONF_BOUNCETIME = 'bouncetime'
CONF_INVERT_LOGIC = 'invert_logic'
CONF_PULL_MODE = 'pull_mode'
DEFAULT_BOUNCETIME = 50
DEFAULT_INVERT_LOGIC = False
DEFAULT_PULL_MODE = 'UP'
PIN_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int,
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE):
vol.In(['UP', 'DOWN'])
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PINS, default={}):
vol.Schema({cv.string: PIN_SCHEMA}),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Beaglebone Black GPIO devices."""
pins = config.get(CONF_PINS)
binary_sensors = []
for pin, params in pins.items():
binary_sensors.append(BBBGPIOBinarySensor(pin, params))
add_devices(binary_sensors)
class BBBGPIOBinarySensor(BinarySensorDevice):
"""Represent a binary sensor that uses Beaglebone Black GPIO."""
def __init__(self, pin, params):
"""Initialize the Beaglebone Black binary sensor."""
self._pin = pin
self._name = params.get(CONF_NAME) or DEVICE_DEFAULT_NAME
self._bouncetime = params.get(CONF_BOUNCETIME)
self._pull_mode = params.get(CONF_PULL_MODE)
self._invert_logic = params.get(CONF_INVERT_LOGIC)
bbb_gpio.setup_input(self._pin, self._pull_mode)
self._state = bbb_gpio.read_input(self._pin)
def read_gpio(pin):
"""Read state from GPIO."""
self._state = bbb_gpio.read_input(self._pin)
self.schedule_update_ha_state()
bbb_gpio.edge_detect(self._pin, read_gpio, self._bouncetime)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return the state of the entity."""
return self._state != self._invert_logic

View File

@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Digital Ocean droplet sensor.""" """Set up the Digital Ocean droplet sensor."""
digital_ocean = get_component('digital_ocean') digital_ocean = get_component('digital_ocean')
droplets = config.get(CONF_DROPLETS) droplets = config.get(CONF_DROPLETS)
@ -68,7 +68,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
return DEFAULT_SENSOR_CLASS return DEFAULT_SENSOR_CLASS
@property @property
def state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the Digital Ocean droplet.""" """Return the state attributes of the Digital Ocean droplet."""
return { return {
ATTR_CREATED_AT: self.data.created_at, ATTR_CREATED_AT: self.data.created_at,

View File

@ -4,8 +4,9 @@ Provides a binary sensor which is a collection of ffmpeg tools.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ffmpeg/ https://home-assistant.io/components/binary_sensor.ffmpeg/
""" """
import asyncio
import logging import logging
from os import path import os
import voluptuous as vol import voluptuous as vol
@ -13,17 +14,22 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN) BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN)
from homeassistant.components.ffmpeg import ( from homeassistant.components.ffmpeg import (
get_binary, run_test, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS) DATA_FFMPEG, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME, from homeassistant.const import (
ATTR_ENTITY_ID) EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, CONF_NAME,
ATTR_ENTITY_ID)
DEPENDENCIES = ['ffmpeg'] DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SERVICE_START = 'ffmpeg_start'
SERVICE_STOP = 'ffmpeg_stop'
SERVICE_RESTART = 'ffmpeg_restart' SERVICE_RESTART = 'ffmpeg_restart'
DATA_FFMPEG_DEVICE = 'ffmpeg_binary_sensor'
FFMPEG_SENSOR_NOISE = 'noise' FFMPEG_SENSOR_NOISE = 'noise'
FFMPEG_SENSOR_MOTION = 'motion' FFMPEG_SENSOR_MOTION = 'motion'
@ -32,6 +38,7 @@ MAP_FFMPEG_BIN = [
FFMPEG_SENSOR_MOTION FFMPEG_SENSOR_MOTION
] ]
CONF_INITIAL_STATE = 'initial_state'
CONF_TOOL = 'tool' CONF_TOOL = 'tool'
CONF_PEAK = 'peak' CONF_PEAK = 'peak'
CONF_DURATION = 'duration' CONF_DURATION = 'duration'
@ -41,10 +48,12 @@ CONF_REPEAT = 'repeat'
CONF_REPEAT_TIME = 'repeat_time' CONF_REPEAT_TIME = 'repeat_time'
DEFAULT_NAME = 'FFmpeg' DEFAULT_NAME = 'FFmpeg'
DEFAULT_INIT_STATE = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN), vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN),
vol.Required(CONF_INPUT): cv.string, vol.Required(CONF_INPUT): cv.string,
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
vol.Optional(CONF_OUTPUT): cv.string, vol.Optional(CONF_OUTPUT): cv.string,
@ -61,7 +70,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Range(min=0)), vol.All(vol.Coerce(int), vol.Range(min=0)),
}) })
SERVICE_RESTART_SCHEMA = vol.Schema({ SERVICE_FFMPEG_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
}) })
@ -72,86 +81,126 @@ def restart(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_RESTART, data) hass.services.call(DOMAIN, SERVICE_RESTART, data)
# list of all ffmpeg sensors @asyncio.coroutine
DEVICES = [] def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the binary sensor.""" """Create the binary sensor."""
from haffmpeg import SensorNoise, SensorMotion from haffmpeg import SensorNoise, SensorMotion
# check source # check source
if not run_test(hass, config.get(CONF_INPUT)): if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
return return
# generate sensor object # generate sensor object
if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE: if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE:
entity = FFmpegNoise(SensorNoise, config) entity = FFmpegNoise(hass, SensorNoise, config)
else: else:
entity = FFmpegMotion(SensorMotion, config) entity = FFmpegMotion(hass, SensorMotion, config)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, entity.shutdown_ffmpeg) @asyncio.coroutine
def async_shutdown(event):
"""Stop ffmpeg."""
yield from entity.async_shutdown_ffmpeg()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, async_shutdown)
# start on startup
if config.get(CONF_INITIAL_STATE):
@asyncio.coroutine
def async_start(event):
"""Start ffmpeg."""
yield from entity.async_start_ffmpeg()
yield from entity.async_update_ha_state()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, async_start)
# add to system # add to system
add_entities([entity]) yield from async_add_devices([entity])
DEVICES.append(entity)
# exists service? # exists service?
if hass.services.has_service(DOMAIN, SERVICE_RESTART): if hass.services.has_service(DOMAIN, SERVICE_RESTART):
hass.data[DATA_FFMPEG_DEVICE].append(entity)
return return
hass.data[DATA_FFMPEG_DEVICE] = [entity]
descriptions = load_yaml_config_file( descriptions = yield from hass.loop.run_in_executor(
path.join(path.dirname(__file__), 'services.yaml')) None, load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
# register service # register service
def _service_handle_restart(service): @asyncio.coroutine
def async_service_handle(service):
"""Handle service binary_sensor.ffmpeg_restart.""" """Handle service binary_sensor.ffmpeg_restart."""
entity_ids = service.data.get('entity_id') entity_ids = service.data.get('entity_id')
if entity_ids: if entity_ids:
_devices = [device for device in DEVICES _devices = [device for device in hass.data[DATA_FFMPEG_DEVICE]
if device.entity_id in entity_ids] if device.entity_id in entity_ids]
else: else:
_devices = DEVICES _devices = hass.data[DATA_FFMPEG_DEVICE]
tasks = []
for device in _devices: for device in _devices:
device.restart_ffmpeg() if service.service == SERVICE_START:
tasks.append(device.async_start_ffmpeg())
elif service.service == SERVICE_STOP:
tasks.append(device.async_shutdown_ffmpeg())
else:
tasks.append(device.async_restart_ffmpeg())
hass.services.register(DOMAIN, SERVICE_RESTART, if tasks:
_service_handle_restart, yield from asyncio.wait(tasks, loop=hass.loop)
descriptions.get(SERVICE_RESTART),
schema=SERVICE_RESTART_SCHEMA) hass.services.async_register(
DOMAIN, SERVICE_START, async_service_handle,
descriptions.get(SERVICE_START), schema=SERVICE_FFMPEG_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_STOP, async_service_handle,
descriptions.get(SERVICE_STOP), schema=SERVICE_FFMPEG_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_RESTART, async_service_handle,
descriptions.get(SERVICE_RESTART), schema=SERVICE_FFMPEG_SCHEMA)
class FFmpegBinarySensor(BinarySensorDevice): class FFmpegBinarySensor(BinarySensorDevice):
"""A binary sensor which use ffmpeg for noise detection.""" """A binary sensor which use ffmpeg for noise detection."""
def __init__(self, ffobj, config): def __init__(self, hass, ffobj, config):
"""Constructor for binary sensor noise detection.""" """Constructor for binary sensor noise detection."""
self._manager = hass.data[DATA_FFMPEG]
self._state = False self._state = False
self._config = config self._config = config
self._name = config.get(CONF_NAME) self._name = config.get(CONF_NAME)
self._ffmpeg = ffobj(get_binary(), self._callback) self._ffmpeg = ffobj(
self._manager.binary, hass.loop, self._async_callback)
self._start_ffmpeg(config) def _async_callback(self, state):
def _callback(self, state):
"""HA-FFmpeg callback for noise detection.""" """HA-FFmpeg callback for noise detection."""
self._state = state self._state = state
self.schedule_update_ha_state() self.hass.async_add_job(self.async_update_ha_state())
def _start_ffmpeg(self, config): def async_start_ffmpeg(self):
"""Start a FFmpeg instance.""" """Start a FFmpeg instance.
raise NotImplementedError
def shutdown_ffmpeg(self, event): This method must be run in the event loop and returns a coroutine.
"""For STOP event to shutdown ffmpeg.""" """
self._ffmpeg.close() raise NotImplementedError()
def restart_ffmpeg(self): def async_shutdown_ffmpeg(self):
"""Restart ffmpeg with new config.""" """For STOP event to shutdown ffmpeg.
self._ffmpeg.close()
self._start_ffmpeg(self._config) This method must be run in the event loop and returns a coroutine.
"""
return self._ffmpeg.close()
@asyncio.coroutine
def async_restart_ffmpeg(self):
"""Restart processing."""
yield from self.async_shutdown_ffmpeg()
yield from self.async_start_ffmpeg()
@property @property
def is_on(self): def is_on(self):
@ -177,20 +226,23 @@ class FFmpegBinarySensor(BinarySensorDevice):
class FFmpegNoise(FFmpegBinarySensor): class FFmpegNoise(FFmpegBinarySensor):
"""A binary sensor which use ffmpeg for noise detection.""" """A binary sensor which use ffmpeg for noise detection."""
def _start_ffmpeg(self, config): def async_start_ffmpeg(self):
"""Start a FFmpeg instance.""" """Start a FFmpeg instance.
This method must be run in the event loop and returns a coroutine.
"""
# init config # init config
self._ffmpeg.set_options( self._ffmpeg.set_options(
time_duration=config.get(CONF_DURATION), time_duration=self._config.get(CONF_DURATION),
time_reset=config.get(CONF_RESET), time_reset=self._config.get(CONF_RESET),
peak=config.get(CONF_PEAK), peak=self._config.get(CONF_PEAK),
) )
# run # run
self._ffmpeg.open_sensor( return self._ffmpeg.open_sensor(
input_source=config.get(CONF_INPUT), input_source=self._config.get(CONF_INPUT),
output_dest=config.get(CONF_OUTPUT), output_dest=self._config.get(CONF_OUTPUT),
extra_cmd=config.get(CONF_EXTRA_ARGUMENTS), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
) )
@property @property
@ -202,20 +254,23 @@ class FFmpegNoise(FFmpegBinarySensor):
class FFmpegMotion(FFmpegBinarySensor): class FFmpegMotion(FFmpegBinarySensor):
"""A binary sensor which use ffmpeg for noise detection.""" """A binary sensor which use ffmpeg for noise detection."""
def _start_ffmpeg(self, config): def async_start_ffmpeg(self):
"""Start a FFmpeg instance.""" """Start a FFmpeg instance.
This method must be run in the event loop and returns a coroutine.
"""
# init config # init config
self._ffmpeg.set_options( self._ffmpeg.set_options(
time_reset=config.get(CONF_RESET), time_reset=self._config.get(CONF_RESET),
time_repeat=config.get(CONF_REPEAT_TIME), time_repeat=self._config.get(CONF_REPEAT_TIME),
repeat=config.get(CONF_REPEAT), repeat=self._config.get(CONF_REPEAT),
changes=config.get(CONF_CHANGES), changes=self._config.get(CONF_CHANGES),
) )
# run # run
self._ffmpeg.open_sensor( return self._ffmpeg.open_sensor(
input_source=config.get(CONF_INPUT), input_source=self._config.get(CONF_INPUT),
extra_cmd=config.get(CONF_EXTRA_ARGUMENTS), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
) )
@property @property

View File

@ -17,6 +17,7 @@ DEPENDENCIES = ['homematic']
SENSOR_TYPES_CLASS = { SENSOR_TYPES_CLASS = {
"Remote": None, "Remote": None,
"ShutterContact": "opening", "ShutterContact": "opening",
"MaxShutterContact": "opening",
"IPShutterContact": "opening", "IPShutterContact": "opening",
"Smoke": "smoke", "Smoke": "smoke",
"SmokeV2": "smoke", "SmokeV2": "smoke",

View File

@ -5,34 +5,39 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.iss/ https://home-assistant.io/components/sensor.iss/
""" """
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta
import requests import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.util import Throttle
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, ATTR_LONGITUDE, ATTR_LATITUDE)
from homeassistant.util import Throttle
REQUIREMENTS = ['pyiss==1.0.1'] REQUIREMENTS = ['pyiss==1.0.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_ISS_VISIBLE = 'visible'
ATTR_ISS_NEXT_RISE = 'next_rise' ATTR_ISS_NEXT_RISE = 'next_rise'
ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space' ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space'
CONF_SHOW_ON_MAP = 'show_on_map'
DEFAULT_NAME = 'ISS' DEFAULT_NAME = 'ISS'
DEFAULT_SENSOR_CLASS = 'visible'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ISS sensor.""" """Set up the ISS sensor."""
# Validate the configuration
if None in (hass.config.latitude, hass.config.longitude): if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False return False
@ -45,75 +50,74 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False return False
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
show_on_map = config.get(CONF_SHOW_ON_MAP)
sensors = [] add_devices([IssBinarySensor(iss_data, name, show_on_map)], True)
sensors.append(IssSensor(iss_data, name))
add_devices(sensors, True)
class IssSensor(Entity): class IssBinarySensor(BinarySensorDevice):
"""Implementation of a ISS sensor.""" """Implementation of the ISS binary sensor."""
def __init__(self, iss_data, name): def __init__(self, iss_data, name, show):
"""Initialize the sensor.""" """Initialize the sensor."""
self.iss_data = iss_data self.iss_data = iss_data
self._state = None self._state = None
self._attributes = {} self._name = name
self._client_name = name self._show_on_map = show
self._name = ATTR_ISS_VISIBLE self.update()
self._unit_of_measurement = None
self._icon = 'mdi:eye'
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return '{} {}'.format(self._client_name, self._name) return self._name
@property @property
def state(self): def is_on(self):
"""Return the state of the sensor.""" """Return true if the binary sensor is on."""
return self._state return self.iss_data.is_above if self.iss_data else False
@property
def sensor_class(self):
"""Return the class of this sensor."""
return DEFAULT_SENSOR_CLASS
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return self._attributes if self.iss_data:
attrs = {
@property ATTR_ISS_NUMBER_PEOPLE_SPACE:
def unit_of_measurement(self): self.iss_data.number_of_people_in_space,
"""Return the unit of measurement of this entity, if any.""" ATTR_ISS_NEXT_RISE: self.iss_data.next_rise,
return self._unit_of_measurement }
if self._show_on_map:
@property attrs[ATTR_LONGITUDE] = self.iss_data.position.get('longitude')
def icon(self): attrs[ATTR_LATITUDE] = self.iss_data.position.get('latitude')
"""Icon to use in the frontend, if any.""" else:
return self._icon attrs['long'] = self.iss_data.position.get('longitude')
attrs['lat'] = self.iss_data.position.get('latitude')
return attrs
def update(self): def update(self):
"""Get the latest data from ISS API and updates the states.""" """Get the latest data from ISS API and updates the states."""
self._state = self.iss_data.is_above self.iss_data.update()
self._attributes[ATTR_ISS_NUMBER_PEOPLE_SPACE] = \
self.iss_data.number_of_people_in_space
delta = self.iss_data.next_rise - datetime.utcnow()
self._attributes[ATTR_ISS_NEXT_RISE] = int(delta.total_seconds() / 60)
class IssData(object): class IssData(object):
"""Get data from the ISS.""" """Get data from the ISS API."""
def __init__(self, latitude, longitude): def __init__(self, latitude, longitude):
"""Initialize the data object.""" """Initialize the data object."""
self.is_above = None self.is_above = None
self.next_rise = None self.next_rise = None
self.number_of_people_in_space = None self.number_of_people_in_space = None
self.position = None
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Get the latest data from the ISS.""" """Get the latest data from the ISS API."""
import pyiss import pyiss
try: try:
@ -121,7 +125,7 @@ class IssData(object):
self.is_above = iss.is_ISS_above(self.latitude, self.longitude) self.is_above = iss.is_ISS_above(self.latitude, self.longitude)
self.next_rise = iss.next_rise(self.latitude, self.longitude) self.next_rise = iss.next_rise(self.latitude, self.longitude)
self.number_of_people_in_space = iss.number_of_people_in_space() self.number_of_people_in_space = iss.number_of_people_in_space()
_LOGGER.error(self.next_rise.tzinfo) self.position = iss.current_location()
except requests.exceptions.HTTPError as error: except requests.exceptions.HTTPError as error:
_LOGGER.error(error) _LOGGER.error(error)
return False return False

View File

@ -7,14 +7,10 @@ https://home-assistant.io/components/binary_sensor.nest/
from itertools import chain from itertools import chain
import logging import logging
import voluptuous as vol from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.sensor.nest import NestSensor from homeassistant.components.sensor.nest import NestSensor
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.components.nest import DATA_NEST from homeassistant.components.nest import DATA_NEST
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
@ -42,17 +38,6 @@ _BINARY_TYPES_DEPRECATED = [
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ _VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \
+ CAMERA_BINARY_TYPES + CAMERA_BINARY_TYPES
_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED = _VALID_BINARY_SENSOR_TYPES \
+ _BINARY_TYPES_DEPRECATED
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list,
[vol.In(_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED)])
})
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -63,15 +48,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return return
nest = hass.data[DATA_NEST] nest = hass.data[DATA_NEST]
conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES)
for variable in conf: # Add all available binary sensors if no Nest binary sensor config is set
if discovery_info == {}:
conditions = _VALID_BINARY_SENSOR_TYPES
else:
conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {})
for variable in conditions:
if variable in _BINARY_TYPES_DEPRECATED: if variable in _BINARY_TYPES_DEPRECATED:
wstr = (variable + " is no a longer supported " wstr = (variable + " is no a longer supported "
"monitored_conditions. See " "monitored_conditions. See "
"https://home-assistant.io/components/binary_sensor.nest/ " "https://home-assistant.io/components/binary_sensor.nest/ "
"for valid options, or remove monitored_conditions " "for valid options.")
"entirely to get a reasonable default")
_LOGGER.error(wstr) _LOGGER.error(wstr)
sensors = [] sensors = []
@ -80,16 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
nest.cameras()) nest.cameras())
for structure, device in device_chain: for structure, device in device_chain:
sensors += [NestBinarySensor(structure, device, variable) sensors += [NestBinarySensor(structure, device, variable)
for variable in conf for variable in conditions
if variable in BINARY_TYPES] if variable in BINARY_TYPES]
sensors += [NestBinarySensor(structure, device, variable) sensors += [NestBinarySensor(structure, device, variable)
for variable in conf for variable in conditions
if variable in CLIMATE_BINARY_TYPES if variable in CLIMATE_BINARY_TYPES
and device.is_thermostat] and device.is_thermostat]
if device.is_camera: if device.is_camera:
sensors += [NestBinarySensor(structure, device, variable) sensors += [NestBinarySensor(structure, device, variable)
for variable in conf for variable in conditions
if variable in CAMERA_BINARY_TYPES] if variable in CAMERA_BINARY_TYPES]
for activity_zone in device.activity_zones: for activity_zone in device.activity_zones:
sensors += [NestActivityZoneSensor(structure, sensors += [NestActivityZoneSensor(structure,

View File

@ -1,19 +1,19 @@
""" """
Support for the Netatmo binary sensors. Support for the Netatmo binary sensors.
The binary sensors based on events seen by the NetatmoCamera The binary sensors based on events seen by the Netatmo cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.netatmo/ https://home-assistant.io/components/binary_sensor.netatmo/.
""" """
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.netatmo import WelcomeData from homeassistant.components.netatmo import CameraData
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT from homeassistant.const import CONF_TIMEOUT, CONF_OFFSET
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ["netatmo"] DEPENDENCIES = ["netatmo"]
@ -22,24 +22,37 @@ _LOGGER = logging.getLogger(__name__)
# These are the available sensors mapped to binary_sensor class # These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = { WELCOME_SENSOR_TYPES = {
"Someone known": 'occupancy', "Someone known": "motion",
"Someone unknown": 'motion', "Someone unknown": "motion",
"Motion": 'motion', "Motion": "motion",
"Tag Vibration": 'vibration', "Tag Vibration": 'vibration',
"Tag Open": 'opening', "Tag Open": 'opening'
}
PRESENCE_SENSOR_TYPES = {
"Outdoor motion": "motion",
"Outdoor human": "motion",
"Outdoor animal": "motion",
"Outdoor vehicle": "motion"
} }
CONF_HOME = 'home' CONF_HOME = 'home'
CONF_CAMERAS = 'cameras' CONF_CAMERAS = 'cameras'
CONF_WELCOME_SENSORS = 'welcome_sensors'
CONF_PRESENCE_SENSORS = 'presence_sensors'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME): cv.string, vol.Optional(CONF_HOME): cv.string,
vol.Optional(CONF_TIMEOUT): cv.positive_int, vol.Optional(CONF_TIMEOUT): cv.positive_int,
vol.Optional(CONF_OFFSET): cv.positive_int,
vol.Optional(CONF_CAMERAS, default=[]): vol.Optional(CONF_CAMERAS, default=[]):
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()): vol.Optional(
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), CONF_WELCOME_SENSORS, default=WELCOME_SENSOR_TYPES.keys()):
vol.All(cv.ensure_list, [vol.In(WELCOME_SENSOR_TYPES)]),
vol.Optional(
CONF_PRESENCE_SENSORS, default=PRESENCE_SENSOR_TYPES.keys()):
vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]),
}) })
@ -49,48 +62,68 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
netatmo = get_component('netatmo') netatmo = get_component('netatmo')
home = config.get(CONF_HOME, None) home = config.get(CONF_HOME, None)
timeout = config.get(CONF_TIMEOUT, 15) timeout = config.get(CONF_TIMEOUT, 15)
offset = config.get(CONF_OFFSET, 90)
module_name = None module_name = None
import lnetatmo import lnetatmo
try: try:
data = WelcomeData(netatmo.NETATMO_AUTH, home) data = CameraData(netatmo.NETATMO_AUTH, home)
if data.get_camera_names() == []: if data.get_camera_names() == []:
return None return None
except lnetatmo.NoDevice: except lnetatmo.NoDevice:
return None return None
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) welcome_sensors = config.get(
CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES)
presence_sensors = config.get(
CONF_PRESENCE_SENSORS, PRESENCE_SENSOR_TYPES)
for camera_name in data.get_camera_names(): for camera_name in data.get_camera_names():
if CONF_CAMERAS in config: camera_type = data.get_camera_type(camera=camera_name, home=home)
if config[CONF_CAMERAS] != [] and \ if camera_type == "NACamera":
camera_name not in config[CONF_CAMERAS]: if CONF_CAMERAS in config:
continue if config[CONF_CAMERAS] != [] and \
for variable in sensors: camera_name not in config[CONF_CAMERAS]:
if variable in ('Tag Vibration', 'Tag Open'): continue
continue for variable in welcome_sensors:
add_devices([WelcomeBinarySensor(data, camera_name, module_name, add_devices([NetatmoBinarySensor(data, camera_name,
home, timeout, variable)]) module_name, home, timeout,
offset, camera_type,
variable)])
if camera_type == "NOC":
if CONF_CAMERAS in config:
if config[CONF_CAMERAS] != [] and \
camera_name not in config[CONF_CAMERAS]:
continue
for variable in presence_sensors:
add_devices([NetatmoBinarySensor(data, camera_name,
module_name, home, timeout,
offset, camera_type,
variable)])
for module_name in data.get_module_names(camera_name): for module_name in data.get_module_names(camera_name):
for variable in sensors: for variable in welcome_sensors:
if variable in ('Tag Vibration', 'Tag Open'): if variable in ('Tag Vibration', 'Tag Open'):
add_devices([WelcomeBinarySensor(data, camera_name, add_devices([NetatmoBinarySensor(data, camera_name,
module_name, home, module_name, home,
timeout, variable)]) timeout, offset,
camera_type,
variable)])
class WelcomeBinarySensor(BinarySensorDevice): class NetatmoBinarySensor(BinarySensorDevice):
"""Represent a single binary sensor in a Netatmo Welcome device.""" """Represent a single binary sensor in a Netatmo Camera device."""
def __init__(self, data, camera_name, module_name, home, timeout, sensor): def __init__(self, data, camera_name, module_name, home,
timeout, offset, camera_type, sensor):
"""Setup for access to the Netatmo camera events.""" """Setup for access to the Netatmo camera events."""
self._data = data self._data = data
self._camera_name = camera_name self._camera_name = camera_name
self._module_name = module_name self._module_name = module_name
self._home = home self._home = home
self._timeout = timeout self._timeout = timeout
self._offset = offset
if home: if home:
self._name = home + ' / ' + camera_name self._name = home + ' / ' + camera_name
else: else:
@ -99,10 +132,11 @@ class WelcomeBinarySensor(BinarySensorDevice):
self._name += ' / ' + module_name self._name += ' / ' + module_name
self._sensor_name = sensor self._sensor_name = sensor
self._name += ' ' + sensor self._name += ' ' + sensor
camera_id = data.welcomedata.cameraByName(camera=camera_name, camera_id = data.camera_data.cameraByName(camera=camera_name,
home=home)['id'] home=home)['id']
self._unique_id = "Welcome_binary_sensor {0} - {1}".format(self._name, self._unique_id = "Netatmo_binary_sensor {0} - {1}".format(self._name,
camera_id) camera_id)
self._cameratype = camera_type
self.update() self.update()
@property @property
@ -118,7 +152,12 @@ class WelcomeBinarySensor(BinarySensorDevice):
@property @property
def sensor_class(self): def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from SENSOR_CLASSES."""
return SENSOR_TYPES.get(self._sensor_name) if self._cameratype == "NACamera":
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
elif self._cameratype == "NOC":
return PRESENCE_SENSOR_TYPES.get(self._sensor_name)
else:
return None
@property @property
def is_on(self): def is_on(self):
@ -130,30 +169,54 @@ class WelcomeBinarySensor(BinarySensorDevice):
self._data.update() self._data.update()
self._data.update_event() self._data.update_event()
if self._sensor_name == "Someone known": if self._cameratype == "NACamera":
self._state =\ if self._sensor_name == "Someone known":
self._data.welcomedata.someoneKnownSeen(self._home, self._state =\
self._data.camera_data.someoneKnownSeen(self._home,
self._camera_name, self._camera_name,
self._timeout*60) self._timeout*60)
elif self._sensor_name == "Someone unknown": elif self._sensor_name == "Someone unknown":
self._state =\ self._state =\
self._data.welcomedata.someoneUnknownSeen(self._home, self._data.camera_data.someoneUnknownSeen(
self._home, self._camera_name, self._timeout*60)
elif self._sensor_name == "Motion":
self._state =\
self._data.camera_data.motionDetected(self._home,
self._camera_name, self._camera_name,
self._timeout*60) self._timeout*60)
elif self._sensor_name == "Motion": else:
self._state =\ return None
self._data.welcomedata.motionDetected(self._home, elif self._cameratype == "NOC":
self._camera_name, if self._sensor_name == "Outdoor motion":
self._timeout*60) self._state =\
self._data.camera_data.outdoormotionDetected(
self._home, self._camera_name, self._offset)
elif self._sensor_name == "Outdoor human":
self._state =\
self._data.camera_data.humanDetected(self._home,
self._camera_name,
self._offset)
elif self._sensor_name == "Outdoor animal":
self._state =\
self._data.camera_data.animalDetected(self._home,
self._camera_name,
self._offset)
elif self._sensor_name == "Outdoor vehicle":
self._state =\
self._data.camera_data.carDetected(self._home,
self._camera_name,
self._offset)
else:
return None
elif self._sensor_name == "Tag Vibration": elif self._sensor_name == "Tag Vibration":
self._state =\ self._state =\
self._data.welcomedata.moduleMotionDetected(self._home, self._data.camera_data.moduleMotionDetected(self._home,
self._module_name, self._module_name,
self._camera_name, self._camera_name,
self._timeout*60) self._timeout*60)
elif self._sensor_name == "Tag Open": elif self._sensor_name == "Tag Open":
self._state =\ self._state =\
self._data.welcomedata.moduleOpened(self._home, self._data.camera_data.moduleOpened(self._home,
self._module_name, self._module_name,
self._camera_name) self._camera_name)
else: else:

View File

@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for port_num, port_name in ports.items(): for port_num, port_name in ports.items():
binary_sensors.append(RPiGPIOBinarySensor( binary_sensors.append(RPiGPIOBinarySensor(
port_name, port_num, pull_mode, bouncetime, invert_logic)) port_name, port_num, pull_mode, bouncetime, invert_logic))
add_devices(binary_sensors) add_devices(binary_sensors, True)
class RPiGPIOBinarySensor(BinarySensorDevice): class RPiGPIOBinarySensor(BinarySensorDevice):
@ -65,9 +65,9 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
self._pull_mode = pull_mode self._pull_mode = pull_mode
self._bouncetime = bouncetime self._bouncetime = bouncetime
self._invert_logic = invert_logic self._invert_logic = invert_logic
self._state = None
rpi_gpio.setup_input(self._port, self._pull_mode) rpi_gpio.setup_input(self._port, self._pull_mode)
self._state = rpi_gpio.read_input(self._port)
def read_gpio(port): def read_gpio(port):
"""Read state from GPIO.""" """Read state from GPIO."""
@ -90,3 +90,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
def is_on(self): def is_on(self):
"""Return the state of the entity.""" """Return the state of the entity."""
return self._state != self._invert_logic return self._state != self._invert_logic
def update(self):
"""Update the GPIO state."""
self._state = rpi_gpio.read_input(self._port)

View File

@ -1,7 +1,23 @@
# Describes the format for available binary_sensor services # Describes the format for available binary_sensor services
ffmpeg_start:
description: Send a start command to a ffmpeg based sensor.
fields:
entity_id:
description: Name(s) of entites that will start. Platform dependent.
example: 'binary_sensor.ffmpeg_noise'
ffmpeg_stop:
description: Send a stop command to a ffmpeg based sensor.
fields:
entity_id:
description: Name(s) of entites that will stop. Platform dependent.
example: 'binary_sensor.ffmpeg_noise'
ffmpeg_restart: ffmpeg_restart:
description: Send a restart command to a ffmpeg based sensor (party mode). description: Send a restart command to a ffmpeg based sensor.
fields: fields:
entity_id: entity_id:

View File

@ -110,7 +110,7 @@ class ThresholdSensor(BinarySensorDevice):
return self._sensor_class return self._sensor_class
@property @property
def state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
return { return {
ATTR_ENTITY_ID: self._entity_id, ATTR_ENTITY_ID: self._entity_id,

View File

@ -8,7 +8,6 @@ at https://home-assistant.io/components/binary_sensor.wink/
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.sensor.wink import WinkDevice from homeassistant.components.sensor.wink import WinkDevice
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
DEPENDENCIES = ['wink'] DEPENDENCIES = ['wink']
@ -43,6 +42,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for hub in pywink.get_hubs(): for hub in pywink.get_hubs():
add_devices([WinkHub(hub, hass)]) add_devices([WinkHub(hub, hass)])
for remote in pywink.get_remotes():
add_devices([WinkRemote(remote, hass)])
for button in pywink.get_buttons():
add_devices([WinkButton(button, hass)])
for gang in pywink.get_gangs():
add_devices([WinkGang(gang, hass)])
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink binary sensor.""" """Representation of a Wink binary sensor."""
@ -50,33 +58,13 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
def __init__(self, wink, hass): def __init__(self, wink, hass):
"""Initialize the Wink binary sensor.""" """Initialize the Wink binary sensor."""
super().__init__(wink, hass) super().__init__(wink, hass)
wink = get_component('wink') self._unit_of_measurement = self.wink.unit()
self._unit_of_measurement = self.wink.UNIT
self.capability = self.wink.capability() self.capability = self.wink.capability()
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
if self.capability == "loudness": return self.wink.state()
state = self.wink.loudness_boolean()
elif self.capability == "vibration":
state = self.wink.vibration_boolean()
elif self.capability == "brightness":
state = self.wink.brightness_boolean()
elif self.capability == "liquid_detected":
state = self.wink.liquid_boolean()
elif self.capability == "motion":
state = self.wink.motion_boolean()
elif self.capability == "presence":
state = self.wink.presence_boolean()
elif self.capability == "co_detected":
state = self.wink.co_detected_boolean()
elif self.capability == "smoke_detected":
state = self.wink.smoke_detected_boolean()
else:
state = self.wink.state()
return state
@property @property
def sensor_class(self): def sensor_class(self):
@ -91,6 +79,11 @@ class WinkHub(WinkDevice, BinarySensorDevice, Entity):
"""Initialize the hub sensor.""" """Initialize the hub sensor."""
WinkDevice.__init__(self, wink, hass) WinkDevice.__init__(self, wink, hass)
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.wink.state()
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
@ -99,7 +92,59 @@ class WinkHub(WinkDevice, BinarySensorDevice, Entity):
'firmware version': self.wink.firmware_version() 'firmware version': self.wink.firmware_version()
} }
class WinkRemote(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink Lutron Connected bulb remote."""
def __init(self, wink, hass):
"""Initialize the hub sensor."""
WinkDevice.__init__(self, wink, hass)
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
return self.wink.state() return self.wink.state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'button_on_pressed': self.wink.button_on_pressed(),
'button_off_pressed': self.wink.button_off_pressed(),
'button_up_pressed': self.wink.button_up_pressed(),
'button_down_pressed': self.wink.button_down_pressed()
}
class WinkButton(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink Relay button."""
def __init(self, wink, hass):
"""Initialize the hub sensor."""
WinkDevice.__init__(self, wink, hass)
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.wink.state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'pressed': self.wink.pressed(),
'long_pressed': self.wink.long_pressed()
}
class WinkGang(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink Relay gang."""
def __init(self, wink, hass):
"""Initialize the gang sensor."""
WinkDevice.__init__(self, wink, hass)
@property
def is_on(self):
"""Return true if the gang is connected."""
return self.wink.state()

View File

@ -8,7 +8,6 @@ import logging
import datetime import datetime
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
from homeassistant.helpers.entity import Entity
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DOMAIN, DOMAIN,
@ -65,21 +64,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ZWaveBinarySensor(value, None)]) add_devices([ZWaveBinarySensor(value, None)])
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity): class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
"""Representation of a binary sensor within Z-Wave.""" """Representation of a binary sensor within Z-Wave."""
def __init__(self, value, sensor_class): def __init__(self, value, sensor_class):
"""Initialize the sensor.""" """Initialize the sensor."""
self._sensor_type = sensor_class self._sensor_type = sensor_class
# pylint: disable=import-error
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
@property @property
def is_on(self): def is_on(self):
"""Return True if the binary sensor is on.""" """Return True if the binary sensor is on."""
@ -95,32 +87,25 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
"""No polling needed.""" """No polling needed."""
return False return False
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
_LOGGER.debug('Value changed for label %s', self._value.label)
self.schedule_update_ha_state()
class ZWaveTriggerSensor(ZWaveBinarySensor):
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
"""Representation of a stateless sensor within Z-Wave.""" """Representation of a stateless sensor within Z-Wave."""
def __init__(self, sensor_value, sensor_class, hass, re_arm_sec=60): def __init__(self, value, sensor_class, hass, re_arm_sec=60):
"""Initialize the sensor.""" """Initialize the sensor."""
super(ZWaveTriggerSensor, self).__init__(sensor_value, sensor_class) super(ZWaveTriggerSensor, self).__init__(value, sensor_class)
self._hass = hass self._hass = hass
self.re_arm_sec = re_arm_sec self.re_arm_sec = re_arm_sec
self.invalidate_after = dt_util.utcnow() + datetime.timedelta( self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
seconds=self.re_arm_sec) seconds=self.re_arm_sec)
# If it's active make sure that we set the timeout tracker # If it's active make sure that we set the timeout tracker
if sensor_value.data: if value.data:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state,
self.invalidate_after) self.invalidate_after)
def value_changed(self, value): def value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value for this entity's node has changed."""
if self._value.value_id == value.value_id: if self._value.value_id == value.value_id:
self.schedule_update_ha_state() self.schedule_update_ha_state()
if value.data: if value.data:

View File

@ -6,14 +6,17 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/ https://home-assistant.io/components/camera/
""" """
import asyncio import asyncio
import collections
from datetime import timedelta from datetime import timedelta
import logging import logging
import hashlib import hashlib
from random import SystemRandom
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
import async_timeout import async_timeout
from homeassistant.core import callback
from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -21,6 +24,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,6 +39,9 @@ STATE_IDLE = 'idle'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom()
@asyncio.coroutine @asyncio.coroutine
def async_get_image(hass, entity_id, timeout=10): def async_get_image(hass, entity_id, timeout=10):
@ -80,6 +87,15 @@ def async_setup(hass, config):
hass.http.register_view(CameraMjpegStream(component.entities)) hass.http.register_view(CameraMjpegStream(component.entities))
yield from component.async_setup(config) yield from component.async_setup(config)
@callback
def update_tokens(time):
"""Update tokens of the entities."""
for entity in component.entities.values():
entity.async_update_token()
hass.async_add_job(entity.async_update_ha_state())
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
return True return True
@ -89,13 +105,8 @@ class Camera(Entity):
def __init__(self): def __init__(self):
"""Initialize a camera.""" """Initialize a camera."""
self.is_streaming = False self.is_streaming = False
self._access_token = hashlib.sha256( self.access_tokens = collections.deque([], 2)
str.encode(str(id(self)))).hexdigest() self.async_update_token()
@property
def access_token(self):
"""Access token for this camera."""
return self._access_token
@property @property
def should_poll(self): def should_poll(self):
@ -105,7 +116,7 @@ class Camera(Entity):
@property @property
def entity_picture(self): def entity_picture(self):
"""Return a link to the camera feed as entity picture.""" """Return a link to the camera feed as entity picture."""
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token) return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
@property @property
def is_recording(self): def is_recording(self):
@ -174,7 +185,7 @@ class Camera(Entity):
yield from asyncio.sleep(.5) yield from asyncio.sleep(.5)
except asyncio.CancelledError: except (asyncio.CancelledError, ConnectionResetError):
_LOGGER.debug("Close stream by frontend.") _LOGGER.debug("Close stream by frontend.")
response = None response = None
@ -196,7 +207,7 @@ class Camera(Entity):
def state_attributes(self): def state_attributes(self):
"""Camera state attributes.""" """Camera state attributes."""
attr = { attr = {
'access_token': self.access_token, 'access_token': self.access_tokens[-1],
} }
if self.model: if self.model:
@ -207,6 +218,13 @@ class Camera(Entity):
return attr return attr
@callback
def async_update_token(self):
"""Update the used token."""
self.access_tokens.append(
hashlib.sha256(
_RND.getrandbits(256).to_bytes(32, 'little')).hexdigest())
class CameraView(HomeAssistantView): class CameraView(HomeAssistantView):
"""Base CameraView.""" """Base CameraView."""
@ -223,10 +241,11 @@ class CameraView(HomeAssistantView):
camera = self.entities.get(entity_id) camera = self.entities.get(entity_id)
if camera is None: if camera is None:
return web.Response(status=404) status = 404 if request[KEY_AUTHENTICATED] else 401
return web.Response(status=status)
authenticated = (request[KEY_AUTHENTICATED] or authenticated = (request[KEY_AUTHENTICATED] or
request.GET.get('token') == camera.access_token) request.GET.get('token') in camera.access_tokens)
if not authenticated: if not authenticated:
return web.Response(status=401) return web.Response(status=401)

View File

@ -4,8 +4,10 @@ This component provides basic support for Amcrest IP cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.amcrest/ https://home-assistant.io/components/camera.amcrest/
""" """
import asyncio
import logging import logging
import aiohttp
import voluptuous as vol import voluptuous as vol
import homeassistant.loader as loader import homeassistant.loader as loader
@ -13,16 +15,20 @@ from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_stream)
REQUIREMENTS = ['amcrest==1.0.0'] REQUIREMENTS = ['amcrest==1.1.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_RESOLUTION = 'resolution' CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
DEFAULT_NAME = 'Amcrest Camera' DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80 DEFAULT_PORT = 80
DEFAULT_RESOLUTION = 'high' DEFAULT_RESOLUTION = 'high'
DEFAULT_STREAM_SOURCE = 'mjpeg'
NOTIFICATION_ID = 'amcrest_notification' NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup' NOTIFICATION_TITLE = 'Amcrest Camera Setup'
@ -32,6 +38,14 @@ RESOLUTION_LIST = {
'low': 1, 'low': 1,
} }
STREAM_SOURCE_LIST = {
'mjpeg': 0,
'snapshot': 1
}
CONTENT_TYPE_HEADER = 'Content-Type'
TIMEOUT = 5
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
@ -40,19 +54,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(vol.In(RESOLUTION_LIST)), vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
vol.All(vol.In(STREAM_SOURCE_LIST)),
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Amcrest IP Camera.""" """Set up an Amcrest IP Camera."""
from amcrest import AmcrestCamera from amcrest import AmcrestCamera
data = AmcrestCamera( camera = AmcrestCamera(
config.get(CONF_HOST), config.get(CONF_PORT), config.get(CONF_HOST), config.get(CONF_PORT),
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera
persistent_notification = loader.get_component('persistent_notification') persistent_notification = loader.get_component('persistent_notification')
try: try:
data.camera.current_time camera.current_time
# pylint: disable=broad-except # pylint: disable=broad-except
except Exception as ex: except Exception as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
@ -64,26 +80,53 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
notification_id=NOTIFICATION_ID) notification_id=NOTIFICATION_ID)
return False return False
add_devices([AmcrestCam(config, data)]) add_devices([AmcrestCam(hass, config, camera)])
return True return True
class AmcrestCam(Camera): class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera.""" """An implementation of an Amcrest IP camera."""
def __init__(self, device_info, data): def __init__(self, hass, device_info, camera):
"""Initialize an Amcrest camera.""" """Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__() super(AmcrestCam, self).__init__()
self._data = data self._camera = camera
self._base_url = self._camera.get_base_url()
self._hass = hass
self._name = device_info.get(CONF_NAME) self._name = device_info.get(CONF_NAME)
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)] self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
self._stream_source = STREAM_SOURCE_LIST[
device_info.get(CONF_STREAM_SOURCE)
]
self._token = self._auth = aiohttp.BasicAuth(
device_info.get(CONF_USERNAME),
password=device_info.get(CONF_PASSWORD)
)
def camera_image(self): def camera_image(self):
"""Return a still image reponse from the camera.""" """Return a still image reponse from the camera."""
# Send the request to snap a picture and return raw jpg data # Send the request to snap a picture and return raw jpg data
response = self._data.camera.snapshot(channel=self._resolution) response = self._camera.snapshot(channel=self._resolution)
return response.data return response.data
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Return an MJPEG stream."""
# The snapshot implementation is handled by the parent class
if self._stream_source == STREAM_SOURCE_LIST['snapshot']:
yield from super().handle_async_mjpeg_stream(request)
return
# Otherwise, stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass)
streaming_url = '{0}mjpg/video.cgi?channel=0&subtype={1}'.format(
self._base_url, self._resolution)
stream_coro = websession.get(
streaming_url, auth=self._token, timeout=TIMEOUT)
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
@property @property
def name(self): def name(self):
"""Return the name of this camera.""" """Return the name of this camera."""

View File

@ -12,10 +12,9 @@ from aiohttp import web
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import ( from homeassistant.components.ffmpeg import (
async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.util.async import run_coroutine_threadsafe
DEPENDENCIES = ['ffmpeg'] DEPENDENCIES = ['ffmpeg']
@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a FFmpeg Camera.""" """Setup a FFmpeg Camera."""
if not async_run_test(hass, config.get(CONF_INPUT)): if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
return return
yield from async_add_devices([FFmpegCamera(hass, config)]) yield from async_add_devices([FFmpegCamera(hass, config)])
@ -44,20 +43,17 @@ class FFmpegCamera(Camera):
def __init__(self, hass, config): def __init__(self, hass, config):
"""Initialize a FFmpeg camera.""" """Initialize a FFmpeg camera."""
super().__init__() super().__init__()
self._manager = hass.data[DATA_FFMPEG]
self._name = config.get(CONF_NAME) self._name = config.get(CONF_NAME)
self._input = config.get(CONF_INPUT) self._input = config.get(CONF_INPUT)
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
@asyncio.coroutine @asyncio.coroutine
def async_camera_image(self): def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
from haffmpeg import ImageSingleAsync, IMAGE_JPEG from haffmpeg import ImageFrame, IMAGE_JPEG
ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop) ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
image = yield from ffmpeg.get_image( image = yield from ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG, self._input, output_format=IMAGE_JPEG,
@ -67,9 +63,9 @@ class FFmpegCamera(Camera):
@asyncio.coroutine @asyncio.coroutine
def handle_async_mjpeg_stream(self, request): def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpegAsync from haffmpeg import CameraMjpeg
stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop) stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
yield from stream.open_camera( yield from stream.open_camera(
self._input, extra_cmd=self._extra_arguments) self._input, extra_cmd=self._extra_arguments)

View File

@ -9,8 +9,6 @@ import logging
from contextlib import closing from contextlib import closing
import aiohttp import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout import async_timeout
import requests import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth from requests.auth import HTTPBasicAuth, HTTPDigestAuth
@ -20,18 +18,21 @@ from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_stream)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_MJPEG_URL = 'mjpeg_url' CONF_MJPEG_URL = 'mjpeg_url'
CONF_STILL_IMAGE_URL = 'still_image_url'
CONTENT_TYPE_HEADER = 'Content-Type' CONTENT_TYPE_HEADER = 'Content-Type'
DEFAULT_NAME = 'Mjpeg Camera' DEFAULT_NAME = 'Mjpeg Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MJPEG_URL): cv.url, vol.Required(CONF_MJPEG_URL): cv.url,
vol.Optional(CONF_STILL_IMAGE_URL): cv.url,
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@ -70,6 +71,7 @@ class MjpegCamera(Camera):
self._username = device_info.get(CONF_USERNAME) self._username = device_info.get(CONF_USERNAME)
self._password = device_info.get(CONF_PASSWORD) self._password = device_info.get(CONF_PASSWORD)
self._mjpeg_url = device_info[CONF_MJPEG_URL] self._mjpeg_url = device_info[CONF_MJPEG_URL]
self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL)
self._auth = None self._auth = None
if self._username and self._password: if self._username and self._password:
@ -78,6 +80,37 @@ class MjpegCamera(Camera):
self._username, password=self._password self._username, password=self._password
) )
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
# DigestAuth is not supported
if self._authentication == HTTP_DIGEST_AUTHENTICATION or \
self._still_image_url is None:
image = yield from self.hass.loop.run_in_executor(
None, self.camera_image)
return image
websession = async_get_clientsession(self.hass)
response = None
try:
with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from websession.get(
self._still_image_url, auth=self._auth)
image = yield from response.read()
return image
except asyncio.TimeoutError:
_LOGGER.error('Timeout getting camera image')
except (aiohttp.errors.ClientError,
aiohttp.errors.ClientDisconnectedError) as err:
_LOGGER.error('Error getting new camera image: %s', err)
finally:
if response is not None:
yield from response.release()
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
if self._username and self._password: if self._username and self._password:
@ -103,36 +136,9 @@ class MjpegCamera(Camera):
# connect to stream # connect to stream
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
stream = None stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
response = None
try:
with async_timeout.timeout(10, loop=self.hass.loop):
stream = yield from websession.get(self._mjpeg_url,
auth=self._auth)
response = web.StreamResponse() yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
yield from response.prepare(request)
while True:
data = yield from stream.content.read(102400)
if not data:
break
response.write(data)
except asyncio.TimeoutError:
raise HTTPGatewayTimeout()
except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.")
response = None
finally:
if stream is not None:
stream.close()
if response is not None:
yield from response.write_eof()
@property @property
def name(self): def name(self):

View File

@ -1,15 +1,15 @@
""" """
Support for the Netatmo Welcome camera. Support for the Netatmo cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.netatmo/ https://home-assistant.io/components/camera.netatmo/.
""" """
import logging import logging
import requests import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.netatmo import WelcomeData from homeassistant.components.netatmo import CameraData
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -30,41 +30,43 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup access to Netatmo Welcome cameras.""" """Setup access to Netatmo cameras."""
netatmo = get_component('netatmo') netatmo = get_component('netatmo')
home = config.get(CONF_HOME) home = config.get(CONF_HOME)
import lnetatmo import lnetatmo
try: try:
data = WelcomeData(netatmo.NETATMO_AUTH, home) data = CameraData(netatmo.NETATMO_AUTH, home)
for camera_name in data.get_camera_names(): for camera_name in data.get_camera_names():
camera_type = data.get_camera_type(camera=camera_name, home=home)
if CONF_CAMERAS in config: if CONF_CAMERAS in config:
if config[CONF_CAMERAS] != [] and \ if config[CONF_CAMERAS] != [] and \
camera_name not in config[CONF_CAMERAS]: camera_name not in config[CONF_CAMERAS]:
continue continue
add_devices([WelcomeCamera(data, camera_name, home)]) add_devices([NetatmoCamera(data, camera_name, home, camera_type)])
except lnetatmo.NoDevice: except lnetatmo.NoDevice:
return None return None
class WelcomeCamera(Camera): class NetatmoCamera(Camera):
"""Representation of the images published from Welcome camera.""" """Representation of the images published from a Netatmo camera."""
def __init__(self, data, camera_name, home): def __init__(self, data, camera_name, home, camera_type):
"""Setup for access to the Netatmo camera images.""" """Setup for access to the Netatmo camera images."""
super(WelcomeCamera, self).__init__() super(NetatmoCamera, self).__init__()
self._data = data self._data = data
self._camera_name = camera_name self._camera_name = camera_name
if home: if home:
self._name = home + ' / ' + camera_name self._name = home + ' / ' + camera_name
else: else:
self._name = camera_name self._name = camera_name
camera_id = data.welcomedata.cameraByName(camera=camera_name, camera_id = data.camera_data.cameraByName(camera=camera_name,
home=home)['id'] home=home)['id']
self._unique_id = "Welcome_camera {0} - {1}".format(self._name, self._unique_id = "Welcome_camera {0} - {1}".format(self._name,
camera_id) camera_id)
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls( self._vpnurl, self._localurl = self._data.camera_data.cameraUrls(
camera=camera_name camera=camera_name
) )
self._cameratype = camera_type
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
@ -79,15 +81,30 @@ class WelcomeCamera(Camera):
_LOGGER.error('Welcome VPN url changed: %s', error) _LOGGER.error('Welcome VPN url changed: %s', error)
self._data.update() self._data.update()
(self._vpnurl, self._localurl) = \ (self._vpnurl, self._localurl) = \
self._data.welcomedata.cameraUrls(camera=self._camera_name) self._data.camera_data.cameraUrls(camera=self._camera_name)
return None return None
return response.content return response.content
@property @property
def name(self): def name(self):
"""Return the name of this Netatmo Welcome device.""" """Return the name of this Netatmo camera device."""
return self._name return self._name
@property
def brand(self):
"""Camera brand."""
return "Netatmo"
@property
def model(self):
"""Camera model."""
if self._cameratype == "NOC":
return "Presence"
elif self._cameratype == "NACamera":
return "Welcome"
else:
return None
@property @property
def unique_id(self): def unique_id(self):
"""Return the unique ID for this sensor.""" """Return the unique ID for this sensor."""

View File

@ -10,8 +10,6 @@ import logging
import voluptuous as vol import voluptuous as vol
import aiohttp import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout import async_timeout
from homeassistant.const import ( from homeassistant.const import (
@ -20,7 +18,8 @@ from homeassistant.const import (
from homeassistant.components.camera import ( from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA) Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_create_clientsession) async_get_clientsession, async_create_clientsession,
async_aiohttp_proxy_stream)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
@ -253,38 +252,10 @@ class SynologyCamera(Camera):
'cameraId': self._camera_id, 'cameraId': self._camera_id,
'format': 'mjpeg' 'format': 'mjpeg'
} }
stream = None stream_coro = self._websession.get(
response = None streaming_url, params=streaming_payload)
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
stream = yield from self._websession.get(
streaming_url,
params=streaming_payload
)
response = web.StreamResponse()
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
yield from response.prepare(request) yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
while True:
data = yield from stream.content.read(102400)
if not data:
break
response.write(data)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", streaming_url)
raise HTTPGatewayTimeout()
except asyncio.CancelledError:
_LOGGER.debug("Close stream by frontend.")
response = None
finally:
if stream is not None:
stream.close()
if response is not None:
yield from response.write_eof()
@property @property
def name(self): def name(self):

View File

@ -32,6 +32,7 @@ SERVICE_SET_AWAY_MODE = "set_away_mode"
SERVICE_SET_AUX_HEAT = "set_aux_heat" SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_TEMPERATURE = "set_temperature" SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode" SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_HOLD_MODE = "set_hold_mode"
SERVICE_SET_OPERATION_MODE = "set_operation_mode" SERVICE_SET_OPERATION_MODE = "set_operation_mode"
SERVICE_SET_SWING_MODE = "set_swing_mode" SERVICE_SET_SWING_MODE = "set_swing_mode"
SERVICE_SET_HUMIDITY = "set_humidity" SERVICE_SET_HUMIDITY = "set_humidity"
@ -56,6 +57,7 @@ ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity" ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity" ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity" ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_HOLD_MODE = "hold_mode"
ATTR_OPERATION_MODE = "operation_mode" ATTR_OPERATION_MODE = "operation_mode"
ATTR_OPERATION_LIST = "operation_list" ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode" ATTR_SWING_MODE = "swing_mode"
@ -93,6 +95,10 @@ SET_FAN_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MODE): cv.string, vol.Required(ATTR_FAN_MODE): cv.string,
}) })
SET_HOLD_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_HOLD_MODE): cv.string,
})
SET_OPERATION_MODE_SCHEMA = vol.Schema({ SET_OPERATION_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_OPERATION_MODE): cv.string, vol.Required(ATTR_OPERATION_MODE): cv.string,
@ -116,9 +122,23 @@ def set_away_mode(hass, away_mode, entity_id=None):
if entity_id: if entity_id:
data[ATTR_ENTITY_ID] = entity_id data[ATTR_ENTITY_ID] = entity_id
_LOGGER.warning(
'This service has been deprecated; use climate.set_hold_mode')
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
def set_hold_mode(hass, hold_mode, entity_id=None):
"""Set new hold mode."""
data = {
ATTR_HOLD_MODE: hold_mode
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data)
def set_aux_heat(hass, aux_heat, entity_id=None): def set_aux_heat(hass, aux_heat, entity_id=None):
"""Turn all or specified climate devices auxillary heater on.""" """Turn all or specified climate devices auxillary heater on."""
data = { data = {
@ -229,6 +249,8 @@ def async_setup(hass, config):
SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE) SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE)
return return
_LOGGER.warning(
'This service has been deprecated; use climate.set_hold_mode')
for climate in target_climate: for climate in target_climate:
if away_mode: if away_mode:
yield from climate.async_turn_away_mode_on() yield from climate.async_turn_away_mode_on()
@ -242,6 +264,23 @@ def async_setup(hass, config):
descriptions.get(SERVICE_SET_AWAY_MODE), descriptions.get(SERVICE_SET_AWAY_MODE),
schema=SET_AWAY_MODE_SCHEMA) schema=SET_AWAY_MODE_SCHEMA)
@asyncio.coroutine
def async_hold_mode_set_service(service):
"""Set hold mode on target climate devices."""
target_climate = component.async_extract_from_service(service)
hold_mode = service.data.get(ATTR_HOLD_MODE)
for climate in target_climate:
yield from climate.async_set_hold_mode(hold_mode)
yield from _async_update_climate(target_climate)
hass.services.async_register(
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
descriptions.get(SERVICE_SET_HOLD_MODE),
schema=SET_HOLD_MODE_SCHEMA)
@asyncio.coroutine @asyncio.coroutine
def async_aux_heat_set_service(service): def async_aux_heat_set_service(service):
"""Set auxillary heater on target climate devices.""" """Set auxillary heater on target climate devices."""
@ -446,6 +485,10 @@ class ClimateDevice(Entity):
if self.operation_list: if self.operation_list:
data[ATTR_OPERATION_LIST] = self.operation_list data[ATTR_OPERATION_LIST] = self.operation_list
is_hold = self.current_hold_mode
if is_hold is not None:
data[ATTR_HOLD_MODE] = is_hold
swing_mode = self.current_swing_mode swing_mode = self.current_swing_mode
if swing_mode is not None: if swing_mode is not None:
data[ATTR_SWING_MODE] = swing_mode data[ATTR_SWING_MODE] = swing_mode
@ -517,6 +560,11 @@ class ClimateDevice(Entity):
"""Return true if away mode is on.""" """Return true if away mode is on."""
return None return None
@property
def current_hold_mode(self):
"""Return the current hold mode, e.g., home, away, temp."""
return None
@property @property
def is_aux_heat_on(self): def is_aux_heat_on(self):
"""Return true if aux heater.""" """Return true if aux heater."""
@ -626,6 +674,18 @@ class ClimateDevice(Entity):
return self.hass.loop.run_in_executor( return self.hass.loop.run_in_executor(
None, self.turn_away_mode_off) None, self.turn_away_mode_off)
def set_hold_mode(self, hold_mode):
"""Set new target hold mode."""
raise NotImplementedError()
def async_set_hold_mode(self, hold_mode):
"""Set new target hold mode.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_hold_mode, hold_mode)
def turn_aux_heat_on(self): def turn_aux_heat_on(self):
"""Turn auxillary heater on.""" """Turn auxillary heater on."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -12,11 +12,11 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo climate devices.""" """Setup the Demo climate devices."""
add_devices([ add_devices([
DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low", DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, None, 77,
None, None, "Auto", "heat", None, None, None), "Auto Low", None, None, "Auto", "heat", None, None, None),
DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High", DemoClimate("Hvac", 21, TEMP_CELSIUS, True, None, 22, "On High",
67, 54, "Off", "cool", False, None, None), 67, 54, "Off", "cool", False, None, None),
DemoClimate("Ecobee", None, TEMP_CELSIUS, None, 23, "Auto Low", DemoClimate("Ecobee", None, TEMP_CELSIUS, None, None, 23, "Auto Low",
None, None, "Auto", "auto", None, 24, 21) None, None, "Auto", "auto", None, 24, 21)
]) ])
@ -25,7 +25,7 @@ class DemoClimate(ClimateDevice):
"""Representation of a demo climate device.""" """Representation of a demo climate device."""
def __init__(self, name, target_temperature, unit_of_measurement, def __init__(self, name, target_temperature, unit_of_measurement,
away, current_temperature, current_fan_mode, away, hold, current_temperature, current_fan_mode,
target_humidity, current_humidity, current_swing_mode, target_humidity, current_humidity, current_swing_mode,
current_operation, aux, target_temp_high, target_temp_low): current_operation, aux, target_temp_high, target_temp_low):
"""Initialize the climate device.""" """Initialize the climate device."""
@ -34,6 +34,7 @@ class DemoClimate(ClimateDevice):
self._target_humidity = target_humidity self._target_humidity = target_humidity
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._away = away self._away = away
self._hold = hold
self._current_temperature = current_temperature self._current_temperature = current_temperature
self._current_humidity = current_humidity self._current_humidity = current_humidity
self._current_fan_mode = current_fan_mode self._current_fan_mode = current_fan_mode
@ -106,6 +107,11 @@ class DemoClimate(ClimateDevice):
"""Return if away mode is on.""" """Return if away mode is on."""
return self._away return self._away
@property
def current_hold_mode(self):
"""Return hold mode setting."""
return self._hold
@property @property
def is_aux_heat_on(self): def is_aux_heat_on(self):
"""Return true if away mode is on.""" """Return true if away mode is on."""
@ -171,6 +177,11 @@ class DemoClimate(ClimateDevice):
self._away = False self._away = False
self.update_ha_state() self.update_ha_state()
def set_hold_mode(self, hold):
"""Update hold mode on."""
self._hold = hold
self.update_ha_state()
def turn_aux_heat_on(self): def turn_aux_heat_on(self):
"""Turn away auxillary heater on.""" """Turn away auxillary heater on."""
self._aux = True self._aux = True

View File

@ -11,10 +11,10 @@ import voluptuous as vol
from homeassistant.components import ecobee from homeassistant.components import ecobee
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH) ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT) ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -145,12 +145,30 @@ class Thermostat(ClimateDevice):
@property @property
def target_temperature_low(self): def target_temperature_low(self):
"""Return the lower bound temperature we try to reach.""" """Return the lower bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredHeat'] / 10) if self.current_operation == STATE_AUTO:
return int(self.thermostat['runtime']['desiredHeat'] / 10)
else:
return None
@property @property
def target_temperature_high(self): def target_temperature_high(self):
"""Return the upper bound temperature we try to reach.""" """Return the upper bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredCool'] / 10) if self.current_operation == STATE_AUTO:
return int(self.thermostat['runtime']['desiredCool'] / 10)
else:
return None
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self.current_operation == STATE_AUTO:
return None
if self.current_operation == STATE_HEAT:
return int(self.thermostat['runtime']['desiredHeat'] / 10)
elif self.current_operation == STATE_COOL:
return int(self.thermostat['runtime']['desiredCool'] / 10)
else:
return None
@property @property
def desired_fan_mode(self): def desired_fan_mode(self):
@ -165,6 +183,19 @@ class Thermostat(ClimateDevice):
else: else:
return STATE_OFF return STATE_OFF
@property
def current_hold_mode(self):
"""Return current hold mode."""
if self.is_away_mode_on:
hold = 'away'
elif self.is_home_mode_on:
hold = 'home'
elif self.is_temp_hold_on():
hold = 'temp'
else:
hold = None
return hold
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation.""" """Return current operation."""
@ -218,54 +249,110 @@ class Thermostat(ClimateDevice):
"fan_min_on_time": self.fan_min_on_time "fan_min_on_time": self.fan_min_on_time
} }
def is_vacation_on(self):
"""Return true if vacation mode is on."""
events = self.thermostat['events']
return any(event['type'] == 'vacation' and event['running']
for event in events)
def is_temp_hold_on(self):
"""Return true if temperature hold is on."""
events = self.thermostat['events']
return any(event['type'] == 'hold' and event['running']
for event in events)
@property @property
def is_away_mode_on(self): def is_away_mode_on(self):
"""Return true if away mode is on.""" """Return true if away mode is on."""
mode = self.mode
events = self.thermostat['events'] events = self.thermostat['events']
for event in events: return any(event['holdClimateRef'] == 'away' or
if event['holdClimateRef'] == 'away' or \ event['type'] == 'autoAway'
event['type'] == 'autoAway': for event in events)
mode = "away"
break
return 'away' in mode
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Turn away on.""" """Turn away on."""
if self.hold_temp: self.data.ecobee.set_climate_hold(self.thermostat_index,
self.data.ecobee.set_climate_hold(self.thermostat_index, "away", self.hold_preference())
"away", "indefinite")
else:
self.data.ecobee.set_climate_hold(self.thermostat_index, "away")
self.update_without_throttle = True self.update_without_throttle = True
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away off.""" """Turn away off."""
self.data.ecobee.resume_program(self.thermostat_index) self.set_hold_mode(None)
@property
def is_home_mode_on(self):
"""Return true if home mode is on."""
events = self.thermostat['events']
return any(event['holdClimateRef'] == 'home' or
event['type'] == 'autoHome'
for event in events)
def turn_home_mode_on(self):
"""Turn home on."""
self.data.ecobee.set_climate_hold(self.thermostat_index,
"home", self.hold_preference())
self.update_without_throttle = True
def set_hold_mode(self, hold_mode):
"""Set hold mode (away, home, temp)."""
hold = self.current_hold_mode
if hold == hold_mode:
return
elif hold_mode == 'away':
self.turn_away_mode_on()
elif hold_mode == 'home':
self.turn_home_mode_on()
elif hold_mode == 'temp':
self.set_temp_hold(int(self.current_temperature))
else:
self.data.ecobee.resume_program(self.thermostat_index)
self.update_without_throttle = True
def set_auto_temp_hold(self, heat_temp, cool_temp):
"""Set temperature hold in auto mode."""
self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp,
heat_temp, self.hold_preference())
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
"cool=%s, is=%s", heat_temp, isinstance(
heat_temp, (int, float)), cool_temp,
isinstance(cool_temp, (int, float)))
self.update_without_throttle = True
def set_temp_hold(self, temp):
"""Set temperature hold in modes other than auto."""
# Set arbitrary range when not in auto mode
if self.current_operation == STATE_HEAT:
heat_temp = temp
cool_temp = temp + 20
elif self.current_operation == STATE_COOL:
heat_temp = temp - 20
cool_temp = temp
self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp,
heat_temp, self.hold_preference())
_LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, "
"cool=%s, is=%s", heat_temp, isinstance(
heat_temp, (int, float)), cool_temp,
isinstance(cool_temp, (int, float)))
self.update_without_throttle = True self.update_without_throttle = True
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \ low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None: high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
high_temp = int(kwargs.get(ATTR_TARGET_TEMP_LOW)) temp = kwargs.get(ATTR_TEMPERATURE)
low_temp = int(kwargs.get(ATTR_TARGET_TEMP_HIGH))
if self.hold_temp: if self.current_operation == STATE_AUTO and low_temp is not None \
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, and high_temp is not None:
high_temp, "indefinite") self.set_auto_temp_hold(int(low_temp), int(high_temp))
_LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, " elif temp is not None:
"high=%s, is=%s", low_temp, isinstance( self.set_temp_hold(int(temp))
low_temp, (int, float)), high_temp,
isinstance(high_temp, (int, float)))
else: else:
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, _LOGGER.error(
high_temp) 'Missing valid arguments for set_temperature in %s', kwargs)
_LOGGER.debug("Setting ecobee temp to: low=%s, is=%s, "
"high=%s, is=%s", low_temp, isinstance(
low_temp, (int, float)), high_temp,
isinstance(high_temp, (int, float)))
self.update_without_throttle = True
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" """Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
@ -284,15 +371,19 @@ class Thermostat(ClimateDevice):
str(resume_all).lower()) str(resume_all).lower())
self.update_without_throttle = True self.update_without_throttle = True
# Home and Sleep mode aren't used in UI yet: def hold_preference(self):
"""Return user preference setting for hold time."""
# Values returned from thermostat are 'useEndTime4hour',
# 'useEndTime2hour', 'nextTransition', 'indefinite', 'askMe'
default = self.thermostat['settings']['holdAction']
if default == 'nextTransition':
return default
elif default == 'indefinite':
return default
else:
return 'nextTransition'
# def turn_home_mode_on(self): # Sleep mode isn't used in UI yet:
# """ Turns home mode on. """
# self.data.ecobee.set_climate_hold(self.thermostat_index, "home")
# def turn_home_mode_off(self):
# """ Turns home mode off. """
# self.data.ecobee.resume_program(self.thermostat_index)
# def turn_sleep_mode_on(self): # def turn_sleep_mode_on(self):
# """ Turns sleep mode on. """ # """ Turns sleep mode on. """

View File

@ -76,6 +76,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
self._name = _name self._name = _name
self._thermostat = eq3.Thermostat(_mac) self._thermostat = eq3.Thermostat(_mac)
@property
def available(self) -> bool:
"""Return if thermostat is available."""
return self.current_operation != STATE_UNKNOWN
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""

View File

@ -87,6 +87,7 @@ class GenericThermostat(ClimateDevice):
self._unit = hass.config.units.temperature_unit self._unit = hass.config.units.temperature_unit
track_state_change(hass, sensor_entity_id, self._sensor_changed) track_state_change(hass, sensor_entity_id, self._sensor_changed)
track_state_change(hass, heater_entity_id, self._switch_changed)
sensor_state = hass.states.get(sensor_entity_id) sensor_state = hass.states.get(sensor_entity_id)
if sensor_state: if sensor_state:
@ -134,7 +135,7 @@ class GenericThermostat(ClimateDevice):
return return
self._target_temp = temperature self._target_temp = temperature
self._control_heating() self._control_heating()
self.update_ha_state() self.schedule_update_ha_state()
@property @property
def min_temp(self): def min_temp(self):
@ -165,6 +166,12 @@ class GenericThermostat(ClimateDevice):
self._control_heating() self._control_heating()
self.schedule_update_ha_state() self.schedule_update_ha_state()
def _switch_changed(self, entity_id, old_state, new_state):
"""Called when heater switch changes state."""
if new_state is None:
return
self.schedule_update_ha_state()
def _update_temp(self, state): def _update_temp(self, state):
"""Update thermostat with latest state from sensor.""" """Update thermostat with latest state from sensor."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['evohomeclient==0.2.5', REQUIREMENTS = ['evohomeclient==0.2.5',
'somecomfort==0.3.2'] 'somecomfort==0.4.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -22,6 +22,18 @@ set_away_mode:
description: New value of away mode description: New value of away mode
example: true example: true
set_hold_mode:
description: Turn hold mode for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
hold_mode:
description: New value of hold mode
example: 'away'
set_temperature: set_temperature:
description: Set target temperature of climate device description: Set target temperature of climate device

View File

@ -52,8 +52,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
def __init__(self, value, temp_unit): def __init__(self, value, temp_unit):
"""Initialize the Z-Wave climate device.""" """Initialize the Z-Wave climate device."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN) ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._index = value.index self._index = value.index
self._node = value.node self._node = value.node
@ -71,9 +69,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
_LOGGER.debug("temp_unit is %s", self._unit) _LOGGER.debug("temp_unit is %s", self._unit)
self._zxt_120 = None self._zxt_120 = None
self.update_properties() self.update_properties()
# register listener
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
# Make sure that we have values for the key before converting to int # Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()): value.node.product_id.strip()):
@ -85,16 +80,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
" workaround") " workaround")
self._zxt_120 = 1 self._zxt_120 = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
_LOGGER.debug('Value changed for label %s', self._value.label)
self.update_properties()
self.schedule_update_ha_state()
def update_properties(self): def update_properties(self):
"""Callback on data change for the registered node/value pair.""" """Callback on data changes for node values."""
# Operation Mode # Operation Mode
for value in self._node.get_values( for value in self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():

View File

@ -53,8 +53,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
def __init__(self, value): def __init__(self, value):
"""Initialize the zwave rollershutter.""" """Initialize the zwave rollershutter."""
import libopenzwave import libopenzwave
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN) ZWaveDeviceEntity.__init__(self, value, DOMAIN)
# pylint: disable=no-member # pylint: disable=no-member
self._lozwmgr = libopenzwave.PyManager() self._lozwmgr = libopenzwave.PyManager()
@ -62,8 +60,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
self._node = value.node self._node = value.node
self._current_position = None self._current_position = None
self._workaround = None self._workaround = None
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
if (value.node.manufacturer_id.strip() and if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()): value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16), specific_sensor_key = (int(value.node.manufacturer_id, 16),
@ -74,16 +70,8 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
_LOGGER.debug("Controller without positioning feedback") _LOGGER.debug("Controller without positioning feedback")
self._workaround = 1 self._workaround = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
_LOGGER.debug('Value changed for label %s', self._value.label)
self.update_properties()
self.schedule_update_ha_state()
def update_properties(self): def update_properties(self):
"""Callback on data change for the registered node/value pair.""" """Callback on data changes for node values."""
# Position value # Position value
for value in self._node.get_values( for value in self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
@ -160,24 +148,12 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
def __init__(self, value): def __init__(self, value):
"""Initialize the zwave garage door.""" """Initialize the zwave garage door."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN) ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._state = value.data
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
_LOGGER.debug('Value changed for label %s', self._value.label)
self._state = value.data
self.schedule_update_ha_state()
@property @property
def is_closed(self): def is_closed(self):
"""Return the current position of Zwave garage door.""" """Return the current position of Zwave garage door."""
return not self._state return not self._value.data
def close_cover(self): def close_cover(self):
"""Close the garage door.""" """Close the garage door."""

View File

@ -71,10 +71,11 @@ _ARP_REGEX = re.compile(
_IP_NEIGH_CMD = 'ip neigh' _IP_NEIGH_CMD = 'ip neigh'
_IP_NEIGH_REGEX = re.compile( _IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' + r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3}|'
r'\w+\s' + r'([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{1,4}){1,7})\s'
r'\w+\s' + r'\w+\s'
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' + r'\w+\s'
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s'
r'(?P<status>(\w+))') r'(?P<status>(\w+))')
_NVRAM_CMD = 'nvram get client_info_tmp' _NVRAM_CMD = 'nvram get client_info_tmp'
@ -323,6 +324,8 @@ class AsusWrtDeviceScanner(DeviceScanner):
else: else:
for lease in result.leases: for lease in result.leases:
if lease.startswith(b'duid '):
continue
match = _LEASES_REGEX.search(lease.decode('utf-8')) match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match: if not match:

View File

@ -15,9 +15,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['https://github.com/deisi/fritzconnection/archive/' REQUIREMENTS = ['fritzconnection==0.6']
'b5c14515e1c8e2652b06b6316a7f3913df942841.zip'
'#fritzconnection==0.4.6']
# Return cached results if last scan was less then this time ago. # Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)

View File

@ -0,0 +1,107 @@
"""
Support for Linksys Access Points.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.linksys_ap/
"""
import base64
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL)
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
INTERFACES = 2
DEFAULT_TIMEOUT = 10
REQUIREMENTS = ['beautifulsoup4==4.5.3']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
def get_scanner(hass, config):
"""Validate the configuration and return a Linksys AP scanner."""
try:
return LinksysAPDeviceScanner(config[DOMAIN])
except ConnectionError:
return None
class LinksysAPDeviceScanner(object):
"""This class queries a Linksys Access Point."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.verify_ssl = config[CONF_VERIFY_SSL]
self.lock = threading.Lock()
self.last_results = []
# Check if the access point is accessible
response = self._make_request()
if not response.status_code == 200:
raise ConnectionError("Cannot connect to Linksys Access Point")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return self.last_results
# pylint: disable=no-self-use
def get_device_name(self, mac):
"""
Return the name (if known) of the device.
Linksys does not provide an API to get a name for a device,
so we just return None
"""
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Check for connected devices."""
from bs4 import BeautifulSoup as BS
with self.lock:
_LOGGER.info("Checking Linksys AP")
self.last_results = []
for interface in range(INTERFACES):
request = self._make_request(interface)
self.last_results.extend(
[x.find_all('td')[1].text
for x in BS(request.content, "html.parser")
.find_all(class_='section-row')]
)
return True
def _make_request(self, unit=0):
# No, the '&&' is not a typo - this is expected by the web interface.
login = base64.b64encode(bytes(self.username, 'utf8')).decode('ascii')
pwd = base64.b64encode(bytes(self.password, 'utf8')).decode('ascii')
return requests.get(
'https://%s/StatusClients.htm&&unit=%s&vap=0' % (self.host, unit),
timeout=DEFAULT_TIMEOUT,
verify=self.verify_ssl,
cookies={'LoginName': login,
'LoginPWD': pwd})

View File

@ -0,0 +1,126 @@
"""
Support for Sky Hub.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.sky_hub/
"""
import logging
import re
import threading
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Return a Sky Hub 5 scanner if successful."""
scanner = SkyHubDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class SkyHubDeviceScanner(DeviceScanner):
"""This class queries a Sky Hub router."""
def __init__(self, config):
"""Initialise the scanner."""
_LOGGER.info("Initialising Sky Hub")
self.host = config.get(CONF_HOST, '192.168.1.254')
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/'.format(self.host)
# Test the router is accessible
data = _get_skyhub_data(self.url)
self.success_init = data is not None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return (device for device in self.last_results)
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
if not self.last_results:
return None
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the Sky Hub is up to date.
Return boolean if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Scanning")
data = _get_skyhub_data(self.url)
if not data:
_LOGGER.warning('Error scanning devices')
return False
self.last_results = data
return True
def _get_skyhub_data(url):
"""Retrieve data from Sky Hub and return parsed result."""
try:
response = requests.get(url, timeout=5)
except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out")
return
if response.status_code == 200:
return _parse_skyhub_response(response.text)
else:
_LOGGER.error("Invalid response from Sky Hub: %s", response)
def _parse_skyhub_response(data_str):
"""Parse the Sky Hub data format."""
pattmatch = re.search('attach_dev = \'(.*)\'', data_str)
patt = pattmatch.group(1)
dev = [patt1.split(',') for patt1 in patt.split('<lf>')]
devices = {}
for dvc in dev:
if _MAC_REGEX.match(dvc[1]):
devices[dvc[1]] = dvc[0]
else:
raise RuntimeError('Error: MAC address ' + dvc[1] +
' not in correct format.')
return devices

View File

@ -0,0 +1,129 @@
"""
Support for Tado Smart Thermostat.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.tado/
"""
import logging
from datetime import timedelta
from collections import namedtuple
import asyncio
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.helpers.aiohttp_client import async_create_clientsession
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string
})
def get_scanner(hass, config):
"""Return a Tado scanner."""
scanner = TadoDeviceScanner(hass, config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "name"])
class TadoDeviceScanner(DeviceScanner):
"""This class gets geofenced devices from Tado."""
def __init__(self, hass, config):
"""Initialize the scanner."""
self.last_results = []
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.tadoapiurl = 'https://my.tado.com/api/v2/me' \
'?username={}&password={}'
self.websession = async_create_clientsession(
hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop))
self.success_init = self._update_info()
_LOGGER.info("Tado scanner initialized")
@asyncio.coroutine
def async_scan_devices(self):
"""Scan for devices and return a list containing found device ids."""
yield from self._update_info()
return [device.mac for device in self.last_results]
@asyncio.coroutine
def async_get_device_name(self, mac):
"""Return the name of the given device or None if we don't know."""
filter_named = [device.name for device in self.last_results
if device.mac == mac]
if filter_named:
return filter_named[0]
else:
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Query Tado for device marked as at home.
Returns boolean if scanning successful.
"""
_LOGGER.debug("Requesting Tado")
last_results = []
response = None
tadojson = None
try:
# get first token
with async_timeout.timeout(10, loop=self.hass.loop):
url = self.tadoapiurl.format(self.username, self.password)
response = yield from self.websession.get(
url
)
# error on Tado webservice
if response.status != 200:
_LOGGER.warning(
"Error %d on %s.", response.status, self.tadoapiurl)
self.token = None
return
tadojson = yield from response.json()
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.error("Can not load Tado data")
return False
finally:
if response is not None:
yield from response.release()
# Find devices that have geofencing enabled, and are currently at home
for mobiledevice in tadojson['mobileDevices']:
if 'location' in mobiledevice:
if mobiledevice['location']['atHome']:
deviceid = mobiledevice['id']
devicename = mobiledevice['name']
last_results.append(Device(deviceid, devicename))
self.last_results = last_results
_LOGGER.info("Tado presence query successful")
return True

View File

@ -12,6 +12,7 @@ import aiohttp
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
@ -29,6 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
CMD_LOGIN = 15 CMD_LOGIN = 15
CMD_LOGOUT = 16
CMD_DEVICES = 123 CMD_DEVICES = 123
@ -62,7 +64,21 @@ class UPCDeviceScanner(DeviceScanner):
} }
self.websession = async_create_clientsession( self.websession = async_create_clientsession(
hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)) hass, auto_cleanup=False,
cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)
)
@asyncio.coroutine
def async_logout(event):
"""Logout from upc connect box."""
try:
yield from self._async_ws_function(CMD_LOGOUT)
self.token = None
finally:
self.websession.detach()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, async_logout)
@asyncio.coroutine @asyncio.coroutine
def async_scan_devices(self): def async_scan_devices(self):
@ -74,12 +90,14 @@ class UPCDeviceScanner(DeviceScanner):
return [] return []
raw = yield from self._async_ws_function(CMD_DEVICES) raw = yield from self._async_ws_function(CMD_DEVICES)
if raw is None:
_LOGGER.warning("Can't read device from %s", self.host)
return
xml_root = ET.fromstring(raw) try:
return [mac.text for mac in xml_root.iter('MACAddr')] xml_root = ET.fromstring(raw)
return [mac.text for mac in xml_root.iter('MACAddr')]
except (ET.ParseError, TypeError):
_LOGGER.warning("Can't read device from %s", self.host)
self.token = None
return []
@asyncio.coroutine @asyncio.coroutine
def async_get_device_name(self, device): def async_get_device_name(self, device):
@ -92,6 +110,7 @@ class UPCDeviceScanner(DeviceScanner):
response = None response = None
try: try:
# get first token # get first token
self.websession.cookie_jar.clear()
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from self.websession.get( response = yield from self.websession.get(
"http://{}/common_page/login.html".format(self.host) "http://{}/common_page/login.html".format(self.host)
@ -107,7 +126,7 @@ class UPCDeviceScanner(DeviceScanner):
}) })
# successfull? # successfull?
if data.find("successful") != -1: if data is not None:
return True return True
return False return False

View File

@ -31,12 +31,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_scanner(hass, config): def get_scanner(hass, config):
"""Validate the configuration and return a Xiaomi Device Scanner.""" """Validate the configuration and return a Xiaomi Device Scanner."""
scanner = XioamiDeviceScanner(config[DOMAIN]) scanner = XiaomiDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
class XioamiDeviceScanner(DeviceScanner): class XiaomiDeviceScanner(DeviceScanner):
"""This class queries a Xiaomi Mi router. """This class queries a Xiaomi Mi router.
Adapted from Luci scanner. Adapted from Luci scanner.
@ -44,15 +44,14 @@ class XioamiDeviceScanner(DeviceScanner):
def __init__(self, config): def __init__(self, config):
"""Initialize the scanner.""" """Initialize the scanner."""
host = config[CONF_HOST] self.host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD] self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock() self.lock = threading.Lock()
self.last_results = {} self.last_results = {}
self.token = _get_token(host, username, password) self.token = _get_token(self.host, self.username, self.password)
self.host = host
self.mac2name = None self.mac2name = None
self.success_init = self.token is not None self.success_init = self.token is not None
@ -66,9 +65,7 @@ class XioamiDeviceScanner(DeviceScanner):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
with self.lock: with self.lock:
if self.mac2name is None: if self.mac2name is None:
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist" result = self._retrieve_list_with_retry()
url = url.format(self.host, self.token)
result = _get_device_list(url)
if result: if result:
hosts = [x for x in result hosts = [x for x in result
if 'mac' in x and 'name' in x] if 'mac' in x and 'name' in x]
@ -76,7 +73,7 @@ class XioamiDeviceScanner(DeviceScanner):
(x['mac'].upper(), x['name']) for x in hosts] (x['mac'].upper(), x['name']) for x in hosts]
self.mac2name = dict(mac2name_list) self.mac2name = dict(mac2name_list)
else: else:
# Error, handled in the _req_json_rpc # Error, handled in the _retrieve_list_with_retry
return return
return self.mac2name.get(device.upper(), None) return self.mac2name.get(device.upper(), None)
@ -90,29 +87,72 @@ class XioamiDeviceScanner(DeviceScanner):
return False return False
with self.lock: with self.lock:
_LOGGER.info('Refreshing device list') result = self._retrieve_list_with_retry()
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
url = url.format(self.host, self.token)
result = _get_device_list(url)
if result: if result:
self.last_results = [] self._store_result(result)
for device_entry in result:
# Check if the device is marked as connected
if int(device_entry['online']) == 1:
self.last_results.append(device_entry['mac'])
return True return True
return False return False
def _retrieve_list_with_retry(self):
"""Retrieve the device list with a retry if token is invalid.
def _get_device_list(url, **kwargs): Return the list if successful.
"""
_LOGGER.info('Refreshing device list')
result = _retrieve_list(self.host, self.token)
if result:
return result
else:
_LOGGER.info('Refreshing token and retrying device list refresh')
self.token = _get_token(self.host, self.username, self.password)
return _retrieve_list(self.host, self.token)
def _store_result(self, result):
"""Extract and store the device list in self.last_results."""
self.last_results = []
for device_entry in result:
# Check if the device is marked as connected
if int(device_entry['online']) == 1:
self.last_results.append(device_entry['mac'])
def _retrieve_list(host, token, **kwargs):
""""Get device list for the given host."""
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
url = url.format(host, token)
try: try:
res = requests.get(url, timeout=5, **kwargs) res = requests.get(url, timeout=5, **kwargs)
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
_LOGGER.exception('Connection to the router timed out') _LOGGER.exception('Connection to the router timed out at URL [%s]',
url)
return
if res.status_code != 200:
_LOGGER.exception('Connection failed with http code [%s]',
res.status_code)
return
try:
result = res.json()
except ValueError:
# If json decoder could not parse the response
_LOGGER.exception('Failed to parse response from mi router')
return
try:
xiaomi_code = result['code']
except KeyError:
_LOGGER.exception('No field code in response from mi router. %s',
result)
return
if xiaomi_code == 0:
try:
return result['list']
except KeyError:
_LOGGER.exception('No list in response from mi router. %s', result)
return
else:
_LOGGER.info(
'Receive wrong Xiaomi code [%s], expected [0] in response [%s]',
xiaomi_code, result)
return return
return _extract_result(res, 'list')
def _get_token(host, username, password): def _get_token(host, username, password):
@ -124,10 +164,6 @@ def _get_token(host, username, password):
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
_LOGGER.exception('Connection to the router timed out') _LOGGER.exception('Connection to the router timed out')
return return
return _extract_result(res, 'token')
def _extract_result(res, key_name):
if res.status_code == 200: if res.status_code == 200:
try: try:
result = res.json() result = res.json()
@ -136,10 +172,12 @@ def _extract_result(res, key_name):
_LOGGER.exception('Failed to parse response from mi router') _LOGGER.exception('Failed to parse response from mi router')
return return
try: try:
return result[key_name] return result['token']
except KeyError: except KeyError:
_LOGGER.exception('No %s in response from mi router. %s', error_message = "Xiaomi token cannot be refreshed, response from "\
key_name, result) + "url: [%s] \nwith parameter: [%s] \nwas: [%s]"
_LOGGER.exception(error_message, url, data, result)
return return
else: else:
_LOGGER.error('Invalid response from mi router: %s', res) _LOGGER.error('Invalid response: [%s] at url: [%s] with data [%s]',
res, url, data)

View File

@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/ https://home-assistant.io/components/emulated_hue/
""" """
import asyncio import asyncio
import json
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -13,6 +14,7 @@ from homeassistant import util
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.components.http import REQUIREMENTS # NOQA
from homeassistant.components.http import HomeAssistantWSGI from homeassistant.components.http import HomeAssistantWSGI
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .hue_api import ( from .hue_api import (
@ -24,8 +26,13 @@ DOMAIN = 'emulated_hue'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NUMBERS_FILE = 'emulated_hue_ids.json'
CONF_HOST_IP = 'host_ip' CONF_HOST_IP = 'host_ip'
CONF_LISTEN_PORT = 'listen_port' CONF_LISTEN_PORT = 'listen_port'
CONF_ADVERTISE_IP = 'advertise_ip'
CONF_ADVERTISE_PORT = 'advertise_port'
CONF_UPNP_BIND_MULTICAST = 'upnp_bind_multicast'
CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_EXPOSED_DOMAINS = 'exposed_domains'
@ -35,18 +42,23 @@ TYPE_ALEXA = 'alexa'
TYPE_GOOGLE = 'google_home' TYPE_GOOGLE = 'google_home'
DEFAULT_LISTEN_PORT = 8300 DEFAULT_LISTEN_PORT = 8300
DEFAULT_UPNP_BIND_MULTICAST = True
DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene'] DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [ DEFAULT_EXPOSED_DOMAINS = [
'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan' 'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan'
] ]
DEFAULT_TYPE = TYPE_ALEXA DEFAULT_TYPE = TYPE_GOOGLE
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_HOST_IP): cv.string, vol.Optional(CONF_HOST_IP): cv.string,
vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_ADVERTISE_IP): cv.string,
vol.Optional(CONF_ADVERTISE_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean,
vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list, vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list,
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
@ -60,7 +72,7 @@ ATTR_EMULATED_HUE = 'emulated_hue'
def setup(hass, yaml_config): def setup(hass, yaml_config):
"""Activate the emulated_hue component.""" """Activate the emulated_hue component."""
config = Config(yaml_config.get(DOMAIN, {})) config = Config(hass, yaml_config.get(DOMAIN, {}))
server = HomeAssistantWSGI( server = HomeAssistantWSGI(
hass, hass,
@ -84,7 +96,9 @@ def setup(hass, yaml_config):
server.register_view(HueOneLightChangeView(config)) server.register_view(HueOneLightChangeView(config))
upnp_listener = UPNPResponderThread( upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port) config.host_ip_addr, config.listen_port,
config.upnp_bind_multicast, config.advertise_ip,
config.advertise_port)
@asyncio.coroutine @asyncio.coroutine
def stop_emulated_hue_bridge(event): def stop_emulated_hue_bridge(event):
@ -108,12 +122,17 @@ def setup(hass, yaml_config):
class Config(object): class Config(object):
"""Holds configuration variables for the emulated hue bridge.""" """Holds configuration variables for the emulated hue bridge."""
def __init__(self, conf): def __init__(self, hass, conf):
"""Initialize the instance.""" """Initialize the instance."""
self.hass = hass
self.type = conf.get(CONF_TYPE) self.type = conf.get(CONF_TYPE)
self.numbers = {} self.numbers = None
self.cached_states = {} self.cached_states = {}
if self.type == TYPE_ALEXA:
_LOGGER.warning('Alexa type is deprecated and will be removed in a'
' future version')
# Get the IP address that will be passed to the Echo during discovery # Get the IP address that will be passed to the Echo during discovery
self.host_ip_addr = conf.get(CONF_HOST_IP) self.host_ip_addr = conf.get(CONF_HOST_IP)
if self.host_ip_addr is None: if self.host_ip_addr is None:
@ -134,6 +153,11 @@ class Config(object):
_LOGGER.warning('When targetting Google Home, listening port has ' _LOGGER.warning('When targetting Google Home, listening port has '
'to be port 80') 'to be port 80')
# Get whether or not UPNP binds to multicast address (239.255.255.250)
# or to the unicast address (host_ip_addr)
self.upnp_bind_multicast = conf.get(
CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST)
# Get domains that cause both "on" and "off" commands to map to "on" # Get domains that cause both "on" and "off" commands to map to "on"
# This is primarily useful for things like scenes or scripts, which # This is primarily useful for things like scenes or scripts, which
# don't really have a concept of being off # don't really have a concept of being off
@ -151,11 +175,21 @@ class Config(object):
self.exposed_domains = conf.get( self.exposed_domains = conf.get(
CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
# Calculated effective advertised IP and port for network isolation
self.advertise_ip = conf.get(
CONF_ADVERTISE_IP) or self.host_ip_addr
self.advertise_port = conf.get(
CONF_ADVERTISE_PORT) or self.listen_port
def entity_id_to_number(self, entity_id): def entity_id_to_number(self, entity_id):
"""Get a unique number for the entity id.""" """Get a unique number for the entity id."""
if self.type == TYPE_ALEXA: if self.type == TYPE_ALEXA:
return entity_id return entity_id
if self.numbers is None:
self.numbers = self._load_numbers_json()
# Google Home # Google Home
for number, ent_id in self.numbers.items(): for number, ent_id in self.numbers.items():
if entity_id == ent_id: if entity_id == ent_id:
@ -163,6 +197,7 @@ class Config(object):
number = str(len(self.numbers) + 1) number = str(len(self.numbers) + 1)
self.numbers[number] = entity_id self.numbers[number] = entity_id
self._save_numbers_json()
return number return number
def number_to_entity_id(self, number): def number_to_entity_id(self, number):
@ -170,6 +205,9 @@ class Config(object):
if self.type == TYPE_ALEXA: if self.type == TYPE_ALEXA:
return number return number
if self.numbers is None:
self.numbers = self._load_numbers_json()
# Google Home # Google Home
assert isinstance(number, str) assert isinstance(number, str)
return self.numbers.get(number) return self.numbers.get(number)
@ -196,3 +234,26 @@ class Config(object):
domain_exposed_by_default and explicit_expose is not False domain_exposed_by_default and explicit_expose is not False
return is_default_exposed or explicit_expose return is_default_exposed or explicit_expose
def _load_numbers_json(self):
"""Helper method to load numbers json."""
try:
with open(self.hass.config.path(NUMBERS_FILE),
encoding='utf-8') as fil:
return json.loads(fil.read())
except (OSError, ValueError) as err:
# OSError if file not found or unaccessible/no permissions
# ValueError if could not parse JSON
if not isinstance(err, FileNotFoundError):
_LOGGER.warning('Failed to open %s: %s', NUMBERS_FILE, err)
return {}
def _save_numbers_json(self):
"""Helper method to save numbers json."""
try:
with open(self.hass.config.path(NUMBERS_FILE), 'w',
encoding='utf-8') as fil:
fil.write(json.dumps(self.numbers))
except OSError as err:
# OSError if file write permissions
_LOGGER.warning('Failed to write %s: %s', NUMBERS_FILE, err)

View File

@ -17,6 +17,10 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_SET,
) )
from homeassistant.components.fan import (
ATTR_SPEED, SUPPORT_SET_SPEED, SPEED_OFF, SPEED_LOW,
SPEED_MEDIUM, SPEED_HIGH
)
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -174,7 +178,9 @@ class HueOneLightChangeView(HomeAssistantView):
# Make sure the entity actually supports brightness # Make sure the entity actually supports brightness
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: if (entity_features &
SUPPORT_BRIGHTNESS &
(entity.domain == "light")) == SUPPORT_BRIGHTNESS:
if brightness is not None: if brightness is not None:
data[ATTR_BRIGHTNESS] = brightness data[ATTR_BRIGHTNESS] = brightness
@ -207,6 +213,23 @@ class HueOneLightChangeView(HomeAssistantView):
else: else:
service = SERVICE_CLOSE_COVER service = SERVICE_CLOSE_COVER
# If the requested entity is a fan, convert to speed
elif entity.domain == "fan":
functions = entity.attributes.get(
ATTR_SUPPORTED_FEATURES, 0)
if (functions & SUPPORT_SET_SPEED) == SUPPORT_SET_SPEED:
if brightness is not None:
domain = entity.domain
# Convert 0-100 to a fan speed
if brightness == 0:
data[ATTR_SPEED] = SPEED_OFF
elif brightness <= 33.3 and brightness > 0:
data[ATTR_SPEED] = SPEED_LOW
elif brightness <= 66.6 and brightness > 33.3:
data[ATTR_SPEED] = SPEED_MEDIUM
elif brightness <= 100 and brightness > 66.6:
data[ATTR_SPEED] = SPEED_HIGH
if entity.domain in config.off_maps_to_on_domains: if entity.domain in config.off_maps_to_on_domains:
# Map the off command to on # Map the off command to on
service = SERVICE_TURN_ON service = SERVICE_TURN_ON
@ -269,7 +292,9 @@ def parse_hue_api_put_light_body(request_json, entity):
report_brightness = True report_brightness = True
result = (brightness > 0) result = (brightness > 0)
elif entity.domain == "script" or entity.domain == "media_player": elif (entity.domain == "script" or
entity.domain == "media_player" or
entity.domain == "fan"):
# Convert 0-255 to 0-100 # Convert 0-255 to 0-100
level = brightness / 255 * 100 level = brightness / 255 * 100
brightness = round(level) brightness = round(level)
@ -299,6 +324,16 @@ def get_entity_state(config, entity):
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0) ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0)
# Convert 0.0-1.0 to 0-255 # Convert 0.0-1.0 to 0-255
final_brightness = round(min(1.0, level) * 255) final_brightness = round(min(1.0, level) * 255)
elif entity.domain == "fan":
speed = entity.attributes.get(ATTR_SPEED, 0)
# Convert 0.0-1.0 to 0-255
final_brightness = 0
if speed == SPEED_LOW:
final_brightness = 85
elif speed == SPEED_MEDIUM:
final_brightness = 170
elif speed == SPEED_HIGH:
final_brightness = 255
else: else:
final_state, final_brightness = cached_state final_state, final_brightness = cached_state
# Make sure brightness is valid # Make sure brightness is valid

View File

@ -50,7 +50,7 @@ class DescriptionXmlView(HomeAssistantView):
""" """
resp_text = xml_template.format( resp_text = xml_template.format(
self.config.host_ip_addr, self.config.listen_port) self.config.advertise_ip, self.config.advertise_port)
return web.Response(text=resp_text, content_type='text/xml') return web.Response(text=resp_text, content_type='text/xml')
@ -60,12 +60,14 @@ class UPNPResponderThread(threading.Thread):
_interrupted = False _interrupted = False
def __init__(self, host_ip_addr, listen_port): def __init__(self, host_ip_addr, listen_port, upnp_bind_multicast,
advertise_ip, advertise_port):
"""Initialize the class.""" """Initialize the class."""
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.host_ip_addr = host_ip_addr self.host_ip_addr = host_ip_addr
self.listen_port = listen_port self.listen_port = listen_port
self.upnp_bind_multicast = upnp_bind_multicast
# Note that the double newline at the end of # Note that the double newline at the end of
# this string is required per the SSDP spec # this string is required per the SSDP spec
@ -80,9 +82,9 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
""" """
self.upnp_response = resp_template.format(host_ip_addr, listen_port) \ self.upnp_response = resp_template.format(
.replace("\n", "\r\n") \ advertise_ip, advertise_port).replace("\n", "\r\n") \
.encode('utf-8') .encode('utf-8')
# Set up a pipe for signaling to the receiver that it's time to # Set up a pipe for signaling to the receiver that it's time to
# shutdown. Essentially, we place the SSDP socket into nonblocking # shutdown. Essentially, we place the SSDP socket into nonblocking
@ -116,7 +118,10 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
socket.inet_aton("239.255.255.250") + socket.inet_aton("239.255.255.250") +
socket.inet_aton(self.host_ip_addr)) socket.inet_aton(self.host_ip_addr))
ssdp_socket.bind(("239.255.255.250", 1900)) if self.upnp_bind_multicast:
ssdp_socket.bind(("239.255.255.250", 1900))
else:
ssdp_socket.bind((self.host_ip_addr, 1900))
while True: while True:
if self._interrupted: if self._interrupted:

View File

@ -41,7 +41,6 @@ SERVICE_SET_DIRECTION = 'set_direction'
SPEED_OFF = 'off' SPEED_OFF = 'off'
SPEED_LOW = 'low' SPEED_LOW = 'low'
SPEED_MED = 'med'
SPEED_MEDIUM = 'medium' SPEED_MEDIUM = 'medium'
SPEED_HIGH = 'high' SPEED_HIGH = 'high'
@ -230,6 +229,9 @@ class FanEntity(ToggleEntity):
def set_speed(self: ToggleEntity, speed: str) -> None: def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
if speed is SPEED_OFF:
self.turn_off()
return
raise NotImplementedError() raise NotImplementedError()
def set_direction(self: ToggleEntity, direction: str) -> None: def set_direction(self: ToggleEntity, direction: str) -> None:
@ -238,6 +240,9 @@ class FanEntity(ToggleEntity):
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn on the fan.""" """Turn on the fan."""
if speed is SPEED_OFF:
self.turn_off()
return
raise NotImplementedError() raise NotImplementedError()
def turn_off(self: ToggleEntity, **kwargs) -> None: def turn_off(self: ToggleEntity, **kwargs) -> None:

View File

@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH, from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
FanEntity, SUPPORT_SET_SPEED, FanEntity, SUPPORT_SET_SPEED,
SUPPORT_OSCILLATE, SUPPORT_DIRECTION) SUPPORT_OSCILLATE, SUPPORT_DIRECTION)
from homeassistant.const import STATE_OFF from homeassistant.const import STATE_OFF
@ -54,9 +54,9 @@ class DemoFan(FanEntity):
@property @property
def speed_list(self) -> list: def speed_list(self) -> list:
"""Get the list of available speeds.""" """Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH] return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def turn_on(self, speed: str=SPEED_MED) -> None: def turn_on(self, speed: str=SPEED_MEDIUM) -> None:
"""Turn on the entity.""" """Turn on the entity."""
self.set_speed(speed) self.set_speed(speed)

View File

@ -8,7 +8,7 @@ import logging
from typing import Callable from typing import Callable
from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF, from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF,
SPEED_LOW, SPEED_MED, SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH) SPEED_HIGH)
import homeassistant.components.isy994 as isy import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_UNKNOWN, STATE_ON, STATE_OFF from homeassistant.const import STATE_UNKNOWN, STATE_ON, STATE_OFF
@ -20,8 +20,8 @@ VALUE_TO_STATE = {
0: SPEED_OFF, 0: SPEED_OFF,
63: SPEED_LOW, 63: SPEED_LOW,
64: SPEED_LOW, 64: SPEED_LOW,
190: SPEED_MED, 190: SPEED_MEDIUM,
191: SPEED_MED, 191: SPEED_MEDIUM,
255: SPEED_HIGH, 255: SPEED_HIGH,
} }
@ -29,7 +29,7 @@ STATE_TO_VALUE = {}
for key in VALUE_TO_STATE: for key in VALUE_TO_STATE:
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
STATES = [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH] STATES = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_MEDIUM, from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH, FanEntity, SPEED_HIGH, FanEntity,
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
SPEED_OFF, ATTR_SPEED) SPEED_OFF, ATTR_SPEED)
@ -64,11 +64,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_OSCILLATION_OFF, vol.Optional(CONF_PAYLOAD_OSCILLATION_OFF,
default=DEFAULT_PAYLOAD_OFF): cv.string, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string,
vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MED): cv.string, vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string,
vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string,
vol.Optional(CONF_SPEED_LIST, vol.Optional(CONF_SPEED_LIST,
default=[SPEED_OFF, SPEED_LOW, default=[SPEED_OFF, SPEED_LOW,
SPEED_MED, SPEED_HIGH]): cv.ensure_list, SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
}) })
@ -162,7 +162,7 @@ class MqttFan(FanEntity):
if payload == self._payload[SPEED_LOW]: if payload == self._payload[SPEED_LOW]:
self._speed = SPEED_LOW self._speed = SPEED_LOW
elif payload == self._payload[SPEED_MEDIUM]: elif payload == self._payload[SPEED_MEDIUM]:
self._speed = SPEED_MED self._speed = SPEED_MEDIUM
elif payload == self._payload[SPEED_HIGH]: elif payload == self._payload[SPEED_HIGH]:
self._speed = SPEED_HIGH self._speed = SPEED_HIGH
self.update_ha_state() self.update_ha_state()
@ -235,11 +235,12 @@ class MqttFan(FanEntity):
"""Return the oscillation state.""" """Return the oscillation state."""
return self._oscillation return self._oscillation
def turn_on(self, speed: str=SPEED_MED) -> None: def turn_on(self, speed: str=None) -> None:
"""Turn on the entity.""" """Turn on the entity."""
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC], mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_ON], self._qos, self._retain) self._payload[STATE_ON], self._qos, self._retain)
self.set_speed(speed) if speed:
self.set_speed(speed)
def turn_off(self) -> None: def turn_off(self) -> None:
"""Turn off the entity.""" """Turn off the entity."""
@ -252,7 +253,7 @@ class MqttFan(FanEntity):
mqtt_payload = SPEED_OFF mqtt_payload = SPEED_OFF
if speed == SPEED_LOW: if speed == SPEED_LOW:
mqtt_payload = self._payload[SPEED_LOW] mqtt_payload = self._payload[SPEED_LOW]
elif speed == SPEED_MED: elif speed == SPEED_MEDIUM:
mqtt_payload = self._payload[SPEED_MEDIUM] mqtt_payload = self._payload[SPEED_MEDIUM]
elif speed == SPEED_HIGH: elif speed == SPEED_HIGH:
mqtt_payload = self._payload[SPEED_HIGH] mqtt_payload = self._payload[SPEED_HIGH]
@ -265,9 +266,12 @@ class MqttFan(FanEntity):
def oscillate(self, oscillating: bool) -> None: def oscillate(self, oscillating: bool) -> None:
"""Set oscillation.""" """Set oscillation."""
if self._topic[CONF_SPEED_COMMAND_TOPIC] is not None: if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None:
self._oscillation = oscillating self._oscillation = oscillating
payload = self._payload[OSCILLATE_ON_PAYLOAD]
if oscillating is False:
payload = self._payload[OSCILLATE_OFF_PAYLOAD]
mqtt.publish(self._hass, mqtt.publish(self._hass,
self._topic[CONF_OSCILLATION_COMMAND_TOPIC], self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
self._oscillation, self._qos, self._retain) payload, self._qos, self._retain)
self.update_ha_state() self.update_ha_state()

View File

@ -50,4 +50,15 @@ toggle:
fields: fields:
entity_id: entity_id:
description: Name(s) of the entities to toggle description: Name(s) of the entities to toggle
exampl: 'fan.living_room' exampl: 'fan.living_room'
set_direction:
description: Set the fan rotation direction
fields:
entity_id:
description: Name(s) of the entities to toggle
exampl: 'fan.living_room'
direction:
description: The direction to rotate
example: 'left'

View File

@ -32,7 +32,7 @@ class WinkFanDevice(WinkDevice, FanEntity):
"""Initialize the fan.""" """Initialize the fan."""
WinkDevice.__init__(self, wink, hass) WinkDevice.__init__(self, wink, hass)
def set_drection(self: ToggleEntity, direction: str) -> None: def set_direction(self: ToggleEntity, direction: str) -> None:
"""Set the direction of the fan.""" """Set the direction of the fan."""
self.wink.set_fan_direction(direction) self.wink.set_fan_direction(direction)

View File

@ -10,13 +10,14 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'ffmpeg' DOMAIN = 'ffmpeg'
REQUIREMENTS = ["ha-ffmpeg==0.15"] REQUIREMENTS = ["ha-ffmpeg==1.2"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_FFMPEG = 'ffmpeg'
CONF_INPUT = 'input' CONF_INPUT = 'input'
CONF_FFMPEG_BIN = 'ffmpeg_bin' CONF_FFMPEG_BIN = 'ffmpeg_bin'
CONF_EXTRA_ARGUMENTS = 'extra_arguments' CONF_EXTRA_ARGUMENTS = 'extra_arguments'
@ -34,53 +35,54 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
FFMPEG_CONFIG = {
CONF_FFMPEG_BIN: DEFAULT_BINARY,
CONF_RUN_TEST: DEFAULT_RUN_TEST,
}
FFMPEG_TEST_CACHE = {}
def setup(hass, config):
"""Setup the FFmpeg component."""
if DOMAIN in config:
FFMPEG_CONFIG.update(config.get(DOMAIN))
return True
def get_binary():
"""Return ffmpeg binary from config.
Async friendly.
"""
return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN)
def run_test(hass, input_source):
"""Run test on this input. TRUE is deactivate or run correct."""
return run_coroutine_threadsafe(
async_run_test(hass, input_source), hass.loop).result()
@asyncio.coroutine @asyncio.coroutine
def async_run_test(hass, input_source): def async_setup(hass, config):
"""Run test on this input. TRUE is deactivate or run correct. """Setup the FFmpeg component."""
conf = config.get(DOMAIN, {})
This method must be run in the event loop. hass.data[DATA_FFMPEG] = FFmpegManager(
""" hass,
from haffmpeg import TestAsync conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY),
conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST)
)
if FFMPEG_CONFIG.get(CONF_RUN_TEST):
# if in cache
if input_source in FFMPEG_TEST_CACHE:
return FFMPEG_TEST_CACHE[input_source]
# run test
ffmpeg_test = TestAsync(get_binary(), loop=hass.loop)
success = yield from ffmpeg_test.run_test(input_source)
if not success:
_LOGGER.error("FFmpeg '%s' test fails!", input_source)
FFMPEG_TEST_CACHE[input_source] = False
return False
FFMPEG_TEST_CACHE[input_source] = True
return True return True
class FFmpegManager(object):
"""Helper for ha-ffmpeg."""
def __init__(self, hass, ffmpeg_bin, run_test):
"""Initialize helper."""
self.hass = hass
self._cache = {}
self._bin = ffmpeg_bin
self._run_test = run_test
@property
def binary(self):
"""Return ffmpeg binary from config."""
return self._bin
@asyncio.coroutine
def async_run_test(self, input_source):
"""Run test on this input. TRUE is deactivate or run correct.
This method must be run in the event loop.
"""
from haffmpeg import Test
if self._run_test:
# if in cache
if input_source in self._cache:
return self._cache[input_source]
# run test
ffmpeg_test = Test(self.binary, loop=self.hass.loop)
success = yield from ffmpeg_test.run_test(input_source)
if not success:
_LOGGER.error("FFmpeg '%s' test fails!", input_source)
self._cache[input_source] = False
return False
self._cache[input_source] = True
return True

View File

@ -1,18 +1,18 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = { FINGERPRINTS = {
"core.js": "22d39af274e1d824ca1302e10971f2d8", "core.js": "769f3fdd4e04b34bd66c7415743cf7b5",
"frontend.html": "61e57194179b27563a05282b58dd4f47", "frontend.html": "d48d9a13f7d677e59b1d22c6db051207",
"mdi.html": "5bb2f1717206bad0d187c2633062c575", "mdi.html": "7a0f14bbf3822449f9060b9c53bd7376",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5", "panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5",
"panels/ha-panel-dev-info.html": "3765a371478cc66d677cf6dcc35267c6", "panels/ha-panel-dev-info.html": "3765a371478cc66d677cf6dcc35267c6",
"panels/ha-panel-dev-service.html": "e32bcd3afdf485417a3e20b4fc760776", "panels/ha-panel-dev-service.html": "1d223225c1c75083738033895ea3e4b5",
"panels/ha-panel-dev-state.html": "8257d99a38358a150eafdb23fa6727e0", "panels/ha-panel-dev-state.html": "8257d99a38358a150eafdb23fa6727e0",
"panels/ha-panel-dev-template.html": "cbb251acabd5e7431058ed507b70522b", "panels/ha-panel-dev-template.html": "cbb251acabd5e7431058ed507b70522b",
"panels/ha-panel-history.html": "7baeb4bd7d9ce0def4f95eab6f10812e", "panels/ha-panel-history.html": "9f2c72574fb6135beb1b381a4b8b7703",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "93de4cee3a2352a6813b5c218421d534", "panels/ha-panel-logbook.html": "313f2ac57aaa5ad55933c9bbf8d8a1e5",
"panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89", "panels/ha-panel-map.html": "13f120066c0b5faa2ce1db2c3f3cc486",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450" "websocket_test.html": "575de64b431fe11c3785bf96d7813450"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 988ac0028163cfc970e781718bc9459ed486ea61 Subproject commit 5159326a7b3d1ba29ae17a7861fa2eaa8c2c95f6

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -75,8 +75,15 @@ Polymer({
}, },
fitMap: function () { fitMap: function () {
var bounds = new window.L.latLngBounds( var bounds;
this._mapItems.map(function (item) { return item.getLatLng(); }));
if (this._mapItems.length === 0) {
bounds = new window.L.latLngBounds(
[window.L.latLng(this.locationGPS.latitude, this.locationGPS.longitude)]);
} else {
bounds = new window.L.latLngBounds(
this._mapItems.map(function (item) { return item.getLatLng(); }));
}
this._map.fitBounds(bounds.pad(0.5)); this._map.fitBounds(bounds.pad(0.5));
}, },

File diff suppressed because one or more lines are too long

View File

@ -1,27 +1,113 @@
""" """
CEC component. HDMI CEC component.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hdmi_cec/ https://home-assistant.io/components/hdmi_cec/
""" """
import logging import logging
import multiprocessing
import os
from collections import defaultdict
from functools import reduce
import voluptuous as vol import voluptuous as vol
from homeassistant.const import (EVENT_HOMEASSISTANT_START, CONF_DEVICES)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant import core
from homeassistant.components import discovery
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_UNKNOWN,
EVENT_HOMEASSISTANT_STOP, STATE_ON,
STATE_OFF, CONF_DEVICES, CONF_PLATFORM,
CONF_CUSTOMIZE, STATE_PLAYING, STATE_IDLE,
STATE_PAUSED, CONF_HOST)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import Entity
_CEC = None REQUIREMENTS = ['pyCEC==0.4.12']
_LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = 'device'
DOMAIN = 'hdmi_cec' DOMAIN = 'hdmi_cec'
MAX_DEPTH = 4 _LOGGER = logging.getLogger(__name__)
DEFAULT_DISPLAY_NAME = "HomeAssistant"
ICON_UNKNOWN = 'mdi:help'
ICON_AUDIO = 'mdi:speaker'
ICON_PLAYER = 'mdi:play'
ICON_TUNER = 'mdi:nest-thermostat'
ICON_RECORDER = 'mdi:microphone'
ICON_TV = 'mdi:television'
ICONS_BY_TYPE = {
0: ICON_TV,
1: ICON_RECORDER,
3: ICON_TUNER,
4: ICON_PLAYER,
5: ICON_AUDIO
}
CEC_DEVICES = defaultdict(list)
CMD_UP = 'up'
CMD_DOWN = 'down'
CMD_MUTE = 'mute'
CMD_UNMUTE = 'unmute'
CMD_MUTE_TOGGLE = 'toggle mute'
CMD_PRESS = 'press'
CMD_RELEASE = 'release'
EVENT_CEC_COMMAND_RECEIVED = 'cec_command_received'
EVENT_CEC_KEYPRESS_RECEIVED = 'cec_keypress_received'
ATTR_PHYSICAL_ADDRESS = 'physical_address'
ATTR_TYPE_ID = 'type_id'
ATTR_VENDOR_NAME = 'vendor_name'
ATTR_VENDOR_ID = 'vendor_id'
ATTR_DEVICE = 'device'
ATTR_COMMAND = 'command'
ATTR_TYPE = 'type'
ATTR_KEY = 'key'
ATTR_DUR = 'dur'
ATTR_SRC = 'src'
ATTR_DST = 'dst'
ATTR_CMD = 'cmd'
ATTR_ATT = 'att'
ATTR_RAW = 'raw'
ATTR_DIR = 'dir'
ATTR_ABT = 'abt'
ATTR_NEW = 'new'
ATTR_ON = 'on'
ATTR_OFF = 'off'
ATTR_TOGGLE = 'toggle'
_VOL_HEX = vol.Any(vol.Coerce(int), lambda x: int(x, 16))
SERVICE_SEND_COMMAND = 'send_command'
SERVICE_SEND_COMMAND_SCHEMA = vol.Schema({
vol.Optional(ATTR_CMD): _VOL_HEX,
vol.Optional(ATTR_SRC): _VOL_HEX,
vol.Optional(ATTR_DST): _VOL_HEX,
vol.Optional(ATTR_ATT): _VOL_HEX,
vol.Optional(ATTR_RAW): vol.Coerce(str)
}, extra=vol.PREVENT_EXTRA)
SERVICE_VOLUME = 'volume'
SERVICE_VOLUME_SCHEMA = vol.Schema({
vol.Optional(CMD_UP): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)),
vol.Optional(CMD_DOWN): vol.Any(CMD_PRESS, CMD_RELEASE, vol.Coerce(int)),
vol.Optional(CMD_MUTE): vol.Any(ATTR_ON, ATTR_OFF, ATTR_TOGGLE),
}, extra=vol.PREVENT_EXTRA)
SERVICE_UPDATE_DEVICES = 'update'
SERVICE_UPDATE_DEVICES_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({})
}, extra=vol.PREVENT_EXTRA)
SERVICE_SELECT_DEVICE = 'select_device'
SERVICE_POWER_ON = 'power_on' SERVICE_POWER_ON = 'power_on'
SERVICE_SELECT_DEVICE = 'select_device'
SERVICE_STANDBY = 'standby' SERVICE_STANDBY = 'standby'
# pylint: disable=unnecessary-lambda # pylint: disable=unnecessary-lambda
@ -30,92 +116,312 @@ DEVICE_SCHEMA = vol.Schema({
cv.string) cv.string)
}) })
CUSTOMIZE_SCHEMA = vol.Schema({
vol.Optional(CONF_PLATFORM, default=MEDIA_PLAYER): vol.Any(MEDIA_PLAYER,
SWITCH)
})
CONF_DISPLAY_NAME = 'osd_name'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_DEVICES): DEVICE_SCHEMA vol.Optional(CONF_DEVICES): vol.Any(DEVICE_SCHEMA,
vol.Schema({
vol.All(cv.string): vol.Any(
cv.string)
})),
vol.Optional(CONF_PLATFORM): vol.Any(SWITCH, MEDIA_PLAYER),
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_DISPLAY_NAME): cv.string,
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
def pad_physical_address(addr):
"""Right-pad a physical address."""
return addr + [0] * (4 - len(addr))
def parse_mapping(mapping, parents=None): def parse_mapping(mapping, parents=None):
"""Parse configuration device mapping.""" """Parse configuration device mapping."""
if parents is None: if parents is None:
parents = [] parents = []
for addr, val in mapping.items(): for addr, val in mapping.items():
cur = parents + [str(addr)] if isinstance(addr, (str,)) and isinstance(val, (str,)):
if isinstance(val, dict): from pycec.network import PhysicalAddress
yield from parse_mapping(val, cur) yield (addr, PhysicalAddress(val))
elif isinstance(val, str): else:
yield (val, cur) cur = parents + [addr]
if isinstance(val, dict):
yield from parse_mapping(val, cur)
elif isinstance(val, str):
yield (val, pad_physical_address(cur))
def pad_physical_address(addr): def setup(hass: HomeAssistant, base_config):
"""Right-pad a physical address."""
return addr + ['0'] * (MAX_DEPTH - len(addr))
def setup(hass, config):
"""Setup CEC capability.""" """Setup CEC capability."""
global _CEC from pycec.network import HDMINetwork
from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand
try: from pycec.const import KEY_VOLUME_UP, KEY_VOLUME_DOWN, KEY_MUTE_ON, \
import cec KEY_MUTE_OFF, KEY_MUTE_TOGGLE, ADDR_AUDIOSYSTEM, ADDR_BROADCAST, \
except ImportError: ADDR_UNREGISTERED
_LOGGER.error("libcec must be installed") from pycec.cec import CecAdapter
return False from pycec.tcp import TcpAdapter
# Parse configuration into a dict of device name to physical address # Parse configuration into a dict of device name to physical address
# represented as a list of four elements. # represented as a list of four elements.
flat = {} device_aliases = {}
for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})): devices = base_config[DOMAIN].get(CONF_DEVICES, {})
flat[pair[0]] = pad_physical_address(pair[1]) _LOGGER.debug("Parsing config %s", devices)
device_aliases.update(parse_mapping(devices))
_LOGGER.debug("Parsed devices: %s", device_aliases)
# Configure libcec. platform = base_config[DOMAIN].get(CONF_PLATFORM, SWITCH)
cfg = cec.libcec_configuration()
cfg.strDeviceName = 'HASS'
cfg.bActivateSource = 0
cfg.bMonitorOnly = 1
cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT
# Setup CEC adapter. loop = (
_CEC = cec.ICECAdapter.Create(cfg) # Create own thread if more than 1 CPU
hass.loop if multiprocessing.cpu_count() < 2 else None)
host = base_config[DOMAIN].get(CONF_HOST, None)
display_name = base_config[DOMAIN].get(CONF_DISPLAY_NAME,
DEFAULT_DISPLAY_NAME)
if host:
adapter = TcpAdapter(host, name=display_name, activate_source=False)
else:
adapter = CecAdapter(name=display_name, activate_source=False)
hdmi_network = HDMINetwork(adapter, loop=loop)
def _power_on(call): def _volume(call):
"""Power on all devices.""" """Increase/decrease volume and mute/unmute system."""
_CEC.PowerOnDevices() mute_key_mapping = {ATTR_TOGGLE: KEY_MUTE_TOGGLE, ATTR_ON: KEY_MUTE_ON,
ATTR_OFF: KEY_MUTE_OFF}
for cmd, att in call.data.items():
if cmd == CMD_UP:
_process_volume(KEY_VOLUME_UP, att)
elif cmd == CMD_DOWN:
_process_volume(KEY_VOLUME_DOWN, att)
elif cmd == CMD_MUTE:
hdmi_network.send_command(
KeyPressCommand(mute_key_mapping[att],
dst=ADDR_AUDIOSYSTEM))
hdmi_network.send_command(
KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM))
_LOGGER.info("Audio muted")
else:
_LOGGER.warning("Unknown command %s", cmd)
def _process_volume(cmd, att):
if isinstance(att, (str,)):
att = att.strip()
if att == CMD_PRESS:
hdmi_network.send_command(
KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM))
elif att == CMD_RELEASE:
hdmi_network.send_command(KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM))
else:
att = 1 if att == "" else int(att)
for _ in range(0, att):
hdmi_network.send_command(
KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM))
hdmi_network.send_command(
KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM))
def _tx(call):
"""Send CEC command."""
data = call.data
if ATTR_RAW in data:
command = CecCommand(data[ATTR_RAW])
else:
if ATTR_SRC in data:
src = data[ATTR_SRC]
else:
src = ADDR_UNREGISTERED
if ATTR_DST in data:
dst = data[ATTR_DST]
else:
dst = ADDR_BROADCAST
if ATTR_CMD in data:
cmd = data[ATTR_CMD]
else:
_LOGGER.error("Attribute 'cmd' is missing")
return False
if ATTR_ATT in data:
if isinstance(data[ATTR_ATT], (list,)):
att = data[ATTR_ATT]
else:
att = reduce(lambda x, y: "%s:%x" % (x, y), data[ATTR_ATT])
else:
att = ""
command = CecCommand(cmd, dst, src, att)
hdmi_network.send_command(command)
@callback
def _standby(call): def _standby(call):
"""Standby all devices.""" hdmi_network.standby()
_CEC.StandbyDevices()
@callback
def _power_on(call):
hdmi_network.power_on()
def _select_device(call): def _select_device(call):
"""Select the active device.""" """Select the active device."""
path = flat.get(call.data[ATTR_DEVICE]) from pycec.network import PhysicalAddress
if not path:
addr = call.data[ATTR_DEVICE]
if not addr:
_LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE])
cmds = [] return
for i in range(1, MAX_DEPTH - 1): if addr in device_aliases:
addr = pad_physical_address(path[:i]) addr = device_aliases[addr]
cmds.append('1f:82:{}{}:{}{}'.format(*addr)) else:
cmds.append('1f:86:{}{}:{}{}'.format(*addr)) entity = hass.states.get(addr)
for cmd in cmds: _LOGGER.debug("Selecting entity %s", entity)
_CEC.Transmit(_CEC.CommandFromString(cmd)) if entity is not None:
_LOGGER.info("Selected %s", call.data[ATTR_DEVICE]) addr = entity.attributes['physical_address']
_LOGGER.debug("Address acquired: %s", addr)
if addr is None:
_LOGGER.error("Device %s has not physical address.",
call.data[ATTR_DEVICE])
return
if not isinstance(addr, (PhysicalAddress,)):
addr = PhysicalAddress(addr)
hdmi_network.active_source(addr)
_LOGGER.info("Selected %s (%s)", call.data[ATTR_DEVICE], addr)
def _update(call):
"""
Callback called when device update is needed.
- called by service, requests CEC network to update data.
"""
hdmi_network.scan()
@callback
def _new_device(device):
"""Called when new device is detected by HDMI network."""
key = DOMAIN + '.' + device.name
hass.data[key] = device
discovery.load_platform(hass, base_config.get(core.DOMAIN).get(
CONF_CUSTOMIZE, {}).get(key, {}).get(CONF_PLATFORM, platform),
DOMAIN, discovered={ATTR_NEW: [key]},
hass_config=base_config)
def _shutdown(call):
hdmi_network.stop()
def _start_cec(event): def _start_cec(event):
"""Open CEC adapter.""" """Register services and start HDMI network to watch for devices."""
adapters = _CEC.DetectAdapters() descriptions = load_yaml_config_file(
if len(adapters) == 0: os.path.join(os.path.dirname(__file__), 'services.yaml'))[DOMAIN]
_LOGGER.error("No CEC adapter found") hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, _tx,
return descriptions[SERVICE_SEND_COMMAND],
SERVICE_SEND_COMMAND_SCHEMA)
hass.services.register(DOMAIN, SERVICE_VOLUME, _volume,
descriptions[SERVICE_VOLUME],
SERVICE_VOLUME_SCHEMA)
hass.services.register(DOMAIN, SERVICE_UPDATE_DEVICES, _update,
descriptions[SERVICE_UPDATE_DEVICES],
SERVICE_UPDATE_DEVICES_SCHEMA)
hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on)
hass.services.register(DOMAIN, SERVICE_STANDBY, _standby)
hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, _select_device)
if _CEC.Open(adapters[0].strComName): hdmi_network.set_new_device_callback(_new_device)
hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) hdmi_network.start()
hass.services.register(DOMAIN, SERVICE_STANDBY, _standby)
hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE,
_select_device)
else:
_LOGGER.error("Failed to open adapter")
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
return True return True
class CecDevice(Entity):
"""Representation of a HDMI CEC device entity."""
def __init__(self, hass: HomeAssistant, device, logical):
"""Initialize the device."""
self._device = device
self.hass = hass
self._icon = None
self._state = STATE_UNKNOWN
self._logical_address = logical
self.entity_id = "%s.%d" % (DOMAIN, self._logical_address)
device.set_update_callback(self._update)
def update(self):
"""Update device status."""
self._update()
def _update(self, device=None):
"""Update device status."""
if device:
from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \
POWER_OFF, POWER_ON
if device.power_status == POWER_OFF:
self._state = STATE_OFF
elif device.status == STATUS_PLAY:
self._state = STATE_PLAYING
elif device.status == STATUS_STOP:
self._state = STATE_IDLE
elif device.status == STATUS_STILL:
self._state = STATE_PAUSED
elif device.power_status == POWER_ON:
self._state = STATE_ON
else:
_LOGGER.warning("Unknown state: %d", device.power_status)
self.schedule_update_ha_state()
@property
def name(self):
"""Return the name of the device."""
return (
"%s %s" % (self.vendor_name, self._device.osd_name)
if (self._device.osd_name is not None and
self.vendor_name is not None and self.vendor_name != 'Unknown')
else "%s %d" % (self._device.type_name, self._logical_address)
if self._device.osd_name is None
else "%s %d (%s)" % (self._device.type_name, self._logical_address,
self._device.osd_name))
@property
def vendor_id(self):
"""ID of device's vendor."""
return self._device.vendor_id
@property
def vendor_name(self):
"""Name of device's vendor."""
return self._device.vendor
@property
def physical_address(self):
"""Physical address of device in HDMI network."""
return str(self._device.physical_address)
@property
def type(self):
"""String representation of device's type."""
return self._device.type_name
@property
def type_id(self):
"""Type ID of device."""
return self._device.type
@property
def icon(self):
"""Icon for device by its type."""
return (self._icon if self._icon is not None else
ICONS_BY_TYPE.get(self._device.type)
if self._device.type in ICONS_BY_TYPE else ICON_UNKNOWN)
@property
def device_state_attributes(self):
"""Return the state attributes."""
state_attr = {}
if self.vendor_id is not None:
state_attr[ATTR_VENDOR_ID] = self.vendor_id
state_attr[ATTR_VENDOR_NAME] = self.vendor_name
if self.type_id is not None:
state_attr[ATTR_TYPE_ID] = self.type_id
state_attr[ATTR_TYPE] = self.type
if self.physical_address is not None:
state_attr[ATTR_PHYSICAL_ADDRESS] = self.physical_address
return state_attr

View File

@ -23,7 +23,7 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.util import Throttle from homeassistant.util import Throttle
DOMAIN = 'homematic' DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.19"] REQUIREMENTS = ["pyhomematic==0.1.20"]
MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300) MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300)
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
@ -68,7 +68,7 @@ HM_DEVICE_TYPES = {
DISCOVER_BINARY_SENSORS: [ DISCOVER_BINARY_SENSORS: [
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact',
'HMWIOSwitch'], 'HMWIOSwitch', 'MaxShutterContact'],
DISCOVER_COVER: ['Blind', 'KeyBlind'] DISCOVER_COVER: ['Blind', 'KeyBlind']
} }
@ -432,8 +432,8 @@ def _system_callback_handler(hass, config, src, *args):
}, config) }, config)
def _get_devices(hass, device_type, keys, proxy): def _get_devices(hass, discovery_type, keys, proxy):
"""Get the Homematic devices.""" """Get the Homematic devices for given discovery_type."""
device_arr = [] device_arr = []
for key in keys: for key in keys:
@ -441,14 +441,14 @@ def _get_devices(hass, device_type, keys, proxy):
class_name = device.__class__.__name__ class_name = device.__class__.__name__
metadata = {} metadata = {}
# is class supported by discovery type # Class supported by discovery type
if class_name not in HM_DEVICE_TYPES[device_type]: if class_name not in HM_DEVICE_TYPES[discovery_type]:
continue continue
# Load metadata if needed to generate a param list # Load metadata if needed to generate a param list
if device_type == DISCOVER_SENSORS: if discovery_type == DISCOVER_SENSORS:
metadata.update(device.SENSORNODE) metadata.update(device.SENSORNODE)
elif device_type == DISCOVER_BINARY_SENSORS: elif discovery_type == DISCOVER_BINARY_SENSORS:
metadata.update(device.BINARYNODE) metadata.update(device.BINARYNODE)
else: else:
metadata.update({None: device.ELEMENT}) metadata.update({None: device.ELEMENT})
@ -459,8 +459,9 @@ def _get_devices(hass, device_type, keys, proxy):
if param in HM_IGNORE_DISCOVERY_NODE: if param in HM_IGNORE_DISCOVERY_NODE:
continue continue
# add devices # Add devices
_LOGGER.debug("Handling %s: %s", param, channels) _LOGGER.debug("%s: Handling %s: %s: %s",
discovery_type, key, param, channels)
for channel in channels: for channel in channels:
name = _create_ha_name( name = _create_ha_name(
name=device.NAME, channel=channel, param=param, name=device.NAME, channel=channel, param=param,
@ -485,7 +486,7 @@ def _get_devices(hass, device_type, keys, proxy):
str(err)) str(err))
else: else:
_LOGGER.debug("Got no params for %s", key) _LOGGER.debug("Got no params for %s", key)
_LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr)) _LOGGER.debug("%s autodiscovery done: %s", discovery_type, str(device_arr))
return device_arr return device_arr
@ -873,7 +874,7 @@ class HMDevice(Entity):
(self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData),
(self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)): (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)):
for node in metadata: for node in metadata:
if node in self._data: if metadata[node] and node in self._data:
self._data[node] = funct(name=node, channel=self._channel) self._data[node] = funct(name=node, channel=self._channel)
return True return True

View File

@ -36,6 +36,7 @@ CONF_SOURCE = 'source'
CONF_CONFIDENCE = 'confidence' CONF_CONFIDENCE = 'confidence'
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
DEFAULT_CONFIDENCE = 80
SOURCE_SCHEMA = vol.Schema({ SOURCE_SCHEMA = vol.Schema({
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
@ -44,6 +45,8 @@ SOURCE_SCHEMA = vol.Schema({
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]), vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]),
vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE):
vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
}) })
SERVICE_SCAN_SCHEMA = vol.Schema({ SERVICE_SCAN_SCHEMA = vol.Schema({
@ -95,6 +98,11 @@ class ImageProcessingEntity(Entity):
"""Return camera entity id from process pictures.""" """Return camera entity id from process pictures."""
return None return None
@property
def confidence(self):
"""Return minimum confidence for do some things."""
return None
def process_image(self, image): def process_image(self, image):
"""Process image.""" """Process image."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -8,13 +8,17 @@ https://home-assistant.io/components/demo/
from homeassistant.components.image_processing import ImageProcessingEntity from homeassistant.components.image_processing import ImageProcessingEntity
from homeassistant.components.image_processing.openalpr_local import ( from homeassistant.components.image_processing.openalpr_local import (
ImageProcessingAlprEntity) ImageProcessingAlprEntity)
from homeassistant.components.image_processing.microsoft_face_identify import (
ImageProcessingFaceIdentifyEntity)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the demo image_processing platform.""" """Setup the demo image_processing platform."""
add_devices([ add_devices([
DemoImageProcessing('camera.demo_camera', "Demo"), DemoImageProcessing('camera.demo_camera', "Demo"),
DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr") DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr"),
DemoImageProcessingFaceIdentify(
'camera.demo_camera', "Demo Face Identify")
]) ])
@ -82,3 +86,39 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity):
} }
self.process_plates(demo_data, 1) self.process_plates(demo_data, 1)
class DemoImageProcessingFaceIdentify(ImageProcessingFaceIdentifyEntity):
"""Demo face identify image processing entity."""
def __init__(self, camera_entity, name):
"""Initialize demo alpr."""
super().__init__()
self._name = name
self._camera = camera_entity
@property
def camera_entity(self):
"""Return camera entity id from process pictures."""
return self._camera
@property
def confidence(self):
"""Return minimum confidence for send events."""
return 80
@property
def name(self):
"""Return the name of the entity."""
return self._name
def process_image(self, image):
"""Process image."""
demo_data = {
'Hans': 98.34,
'Helena': 82.53,
'Luna': 62.53,
}
self.process_faces(demo_data, 4)

View File

@ -0,0 +1,193 @@
"""
Component that will help set the microsoft face for verify processing.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/image_processing.microsoft_face_identify/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import split_entity_id, callback
from homeassistant.const import STATE_UNKNOWN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE
from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_callback_threadsafe
DEPENDENCIES = ['microsoft_face']
_LOGGER = logging.getLogger(__name__)
EVENT_IDENTIFY_FACE = 'identify_face'
ATTR_NAME = 'name'
ATTR_TOTAL_FACES = 'total_faces'
ATTR_KNOWN_FACES = 'known_faces'
CONF_GROUP = 'group'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_GROUP): cv.slugify,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the microsoft face identify platform."""
api = hass.data[DATA_MICROSOFT_FACE]
face_group = config[CONF_GROUP]
confidence = config[CONF_CONFIDENCE]
entities = []
for camera in config[CONF_SOURCE]:
entities.append(MicrosoftFaceIdentifyEntity(
camera[CONF_ENTITY_ID], api, face_group, confidence,
camera.get(CONF_NAME)
))
yield from async_add_devices(entities)
class ImageProcessingFaceIdentifyEntity(ImageProcessingEntity):
"""Base entity class for face identify/verify image processing."""
def __init__(self):
"""Initialize base face identify/verify entity."""
self.known_faces = {} # last scan data
self.total_faces = 0 # face count
@property
def state(self):
"""Return the state of the entity."""
confidence = 0
face_name = STATE_UNKNOWN
# search high verify face
for i_name, i_co in self.known_faces.items():
if i_co > confidence:
confidence = i_co
face_name = i_name
return face_name
@property
def state_attributes(self):
"""Return device specific state attributes."""
attr = {
ATTR_KNOWN_FACES: self.known_faces,
ATTR_TOTAL_FACES: self.total_faces,
}
return attr
def process_faces(self, known, total):
"""Send event with detected faces and store data."""
run_callback_threadsafe(
self.hass.loop, self.async_process_faces, known, total
).result()
@callback
def async_process_faces(self, known, total):
"""Send event with detected faces and store data.
known are a dict in follow format:
{ 'name': confidence }
This method must be run in the event loop.
"""
detect = {name: confidence for name, confidence in known.items()
if confidence >= self.confidence}
# send events
for name, confidence in detect.items():
self.hass.async_add_job(
self.hass.bus.async_fire, EVENT_IDENTIFY_FACE, {
ATTR_NAME: name,
ATTR_ENTITY_ID: self.entity_id,
ATTR_CONFIDENCE: confidence,
}
)
# update entity store
self.known_faces = detect
self.total_faces = total
class MicrosoftFaceIdentifyEntity(ImageProcessingFaceIdentifyEntity):
"""Microsoft face api entity for identify."""
def __init__(self, camera_entity, api, face_group, confidence, name=None):
"""Initialize openalpr local api."""
super().__init__()
self._api = api
self._camera = camera_entity
self._confidence = confidence
self._face_group = face_group
if name:
self._name = name
else:
self._name = "MicrosoftFace {0}".format(
split_entity_id(camera_entity)[1])
@property
def confidence(self):
"""Return minimum confidence for send events."""
return self._confidence
@property
def camera_entity(self):
"""Return camera entity id from process pictures."""
return self._camera
@property
def name(self):
"""Return the name of the entity."""
return self._name
@asyncio.coroutine
def async_process_image(self, image):
"""Process image.
This method is a coroutine.
"""
detect = None
try:
face_data = yield from self._api.call_api(
'post', 'detect', image, binary=True)
if face_data is None or len(face_data) < 1:
return
face_ids = [data['faceId'] for data in face_data]
detect = yield from self._api.call_api(
'post', 'identify',
{'faceIds': face_ids, 'personGroupId': self._face_group})
except HomeAssistantError as err:
_LOGGER.error("Can't process image on microsoft face: %s", err)
return
# parse data
knwon_faces = {}
total = 0
for face in detect:
total += 1
if len(face['candidates']) == 0:
continue
data = face['candidates'][0]
name = ''
for s_name, s_id in self._api.store[self._face_group].items():
if data['personId'] == s_id:
name = s_name
break
knwon_faces[name] = data['confidence'] * 100
# process data
self.async_process_faces(knwon_faces, total)

View File

@ -26,25 +26,26 @@ _LOGGER = logging.getLogger(__name__)
OPENALPR_API_URL = "https://api.openalpr.com/v1/recognize" OPENALPR_API_URL = "https://api.openalpr.com/v1/recognize"
OPENALPR_REGIONS = [ OPENALPR_REGIONS = [
'us',
'eu',
'au', 'au',
'auwide', 'auwide',
'br',
'eu',
'fr',
'gb', 'gb',
'kr', 'kr',
'kr2',
'mx', 'mx',
'sg', 'sg',
'us',
'vn2'
] ]
CONF_REGION = 'region' CONF_REGION = 'region'
DEFAULT_CONFIDENCE = 80
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_REGION): vol.Required(CONF_REGION):
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)), vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE):
vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
}) })

View File

@ -31,28 +31,30 @@ ATTR_PLATES = 'plates'
ATTR_VEHICLES = 'vehicles' ATTR_VEHICLES = 'vehicles'
OPENALPR_REGIONS = [ OPENALPR_REGIONS = [
'us',
'eu',
'au', 'au',
'auwide', 'auwide',
'br',
'eu',
'fr',
'gb', 'gb',
'kr', 'kr',
'kr2',
'mx', 'mx',
'sg', 'sg',
'us',
'vn2'
] ]
CONF_REGION = 'region' CONF_REGION = 'region'
CONF_ALPR_BIN = 'alp_bin' CONF_ALPR_BIN = 'alp_bin'
DEFAULT_BINARY = 'alpr' DEFAULT_BINARY = 'alpr'
DEFAULT_CONFIDENCE = 80
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_REGION): vol.Required(CONF_REGION):
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)), vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
vol.Optional(CONF_ALPR_BIN, default=DEFAULT_BINARY): cv.string, vol.Optional(CONF_ALPR_BIN, default=DEFAULT_BINARY): cv.string,
vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE):
vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
}) })
@ -79,11 +81,6 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
self.plates = {} # last scan data self.plates = {} # last scan data
self.vehicles = 0 # vehicles count self.vehicles = 0 # vehicles count
@property
def confidence(self):
"""Return minimum confidence for send events."""
return None
@property @property
def state(self): def state(self):
"""Return the state of the entity.""" """Return the state of the entity."""

View File

@ -244,9 +244,7 @@ def setup(hass, config):
if CONFIG_FILE == {}: if CONFIG_FILE == {}:
CONFIG_FILE[ATTR_DEVICES] = {} CONFIG_FILE[ATTR_DEVICES] = {}
# Notify needs to have discovery discovery.load_platform(hass, "notify", DOMAIN, {}, config)
# notify_config = {"notify": {CONF_PLATFORM: "ios"}}
# bootstrap.setup_component(hass, "notify", notify_config)
discovery.load_platform(hass, "sensor", DOMAIN, {}, config) discovery.load_platform(hass, "sensor", DOMAIN, {}, config)

View File

@ -228,10 +228,6 @@ class ISYDevice(Entity):
self._change_handler = self._node.status.subscribe('changed', self._change_handler = self._node.status.subscribe('changed',
self.on_update) self.on_update)
def __del__(self) -> None:
"""Cleanup the subscriptions."""
self._change_handler.unsubscribe()
# pylint: disable=unused-argument # pylint: disable=unused-argument
def on_update(self, event: object) -> None: def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node.""" """Handle the update event from the ISY994 Node."""
@ -272,7 +268,7 @@ class ISYDevice(Entity):
return self._node.status._val return self._node.status._val
@property @property
def state_attributes(self) -> Dict: def device_state_attributes(self) -> Dict:
"""Get the state attributes for the device.""" """Get the state attributes for the device."""
attr = {} attr = {}
if hasattr(self._node, 'aux_properties'): if hasattr(self._node, 'aux_properties'):

View File

@ -1,32 +1,8 @@
""" """
Receive signals from a keyboard and use it as a remote control. Receive signals from a keyboard and use it as a remote control.
This component allows to use a keyboard as remote control. It will For more details about this platform, please refer to the documentation at
fire ´keyboard_remote_command_received´ events witch can then be used https://home-assistant.io/components/keyboard_remote/
in automation rules.
The `evdev` package is used to interface with the keyboard and thus this
is Linux only. It also means you can't use your normal keyboard for this,
because `evdev` will block it.
Example:
keyboard_remote:
device_descriptor: '/dev/input/by-id/foo'
type: 'key_up' # optional alternaive 'key_down' and 'key_hold'
# be carefull, 'key_hold' fires a lot of events
and an automation rule to bring breath live into it.
automation:
alias: Keyboard All light on
trigger:
platform: event
event_type: keyboard_remote_command_received
event_data:
key_code: 107 # inspect log to obtain desired keycode
action:
service: light.turn_on
entity_id: light.all
""" """
# pylint: disable=import-error # pylint: disable=import-error
@ -48,14 +24,20 @@ REQUIREMENTS = ['evdev==0.6.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ICON = 'mdi:remote' ICON = 'mdi:remote'
KEYBOARD_REMOTE_COMMAND_RECEIVED = 'keyboard_remote_command_received' KEYBOARD_REMOTE_COMMAND_RECEIVED = 'keyboard_remote_command_received'
KEYBOARD_REMOTE_CONNECTED = 'keyboard_remote_connected'
KEYBOARD_REMOTE_DISCONNECTED = 'keyboard_remote_disconnected'
KEY_CODE = 'key_code' KEY_CODE = 'key_code'
KEY_VALUE = {'key_up': 0, 'key_down': 1, 'key_hold': 2} KEY_VALUE = {'key_up': 0, 'key_down': 1, 'key_hold': 2}
TYPE = 'type' TYPE = 'type'
DEVICE_DESCRIPTOR = 'device_descriptor' DEVICE_DESCRIPTOR = 'device_descriptor'
DEVICE_NAME = 'device_name'
DEVICE_ID_GROUP = 'Device descriptor or name'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(DEVICE_DESCRIPTOR): cv.string, 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.Optional(TYPE, default='key_up'):
vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold')), vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold')),
}), }),
@ -65,22 +47,15 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Setup keyboard_remote.""" """Setup keyboard_remote."""
config = config.get(DOMAIN) config = config.get(DOMAIN)
device_descriptor = config.get(DEVICE_DESCRIPTOR)
if not device_descriptor:
id_folder = '/dev/input/'
_LOGGER.error(
'A device_descriptor must be defined. '
'Possible descriptors are %s:\n%s',
id_folder, os.listdir(id_folder)
)
return
key_value = KEY_VALUE.get(config.get(TYPE, 'key_up')) 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( keyboard_remote = KeyboardRemote(
hass, hass,
device_descriptor, config
key_value
) )
def _start_keyboard_remote(_event): def _start_keyboard_remote(_event):
@ -104,60 +79,93 @@ def setup(hass, config):
class KeyboardRemote(threading.Thread): class KeyboardRemote(threading.Thread):
"""This interfaces with the inputdevice using evdev.""" """This interfaces with the inputdevice using evdev."""
def __init__(self, hass, device_descriptor, key_value): def __init__(self, hass, config):
"""Construct a KeyboardRemote interface object.""" """Construct a KeyboardRemote interface object."""
from evdev import InputDevice from evdev import InputDevice, list_devices
self.device_descriptor = device_descriptor self.device_descriptor = config.get(DEVICE_DESCRIPTOR)
try: self.device_name = config.get(DEVICE_NAME)
self.dev = InputDevice(device_descriptor) if self.device_descriptor:
except OSError: # Keyboard not present self.device_id = self.device_descriptor
_LOGGER.debug(
'KeyboardRemote: keyboard not connected, %s',
self.device_descriptor)
self.keyboard_connected = False
else: else:
self.keyboard_connected = True self.device_id = self.device_name
self.dev = self._get_keyboard_device()
if self.dev is not None:
_LOGGER.debug( _LOGGER.debug(
'KeyboardRemote: keyboard connected, %s', 'Keyboard connected, %s',
self.dev) self.device_id
)
else:
id_folder = '/dev/input/by-id/'
device_names = [InputDevice(file_name).name
for file_name in list_devices()]
_LOGGER.debug(
'Keyboard not connected, %s.\n\
Check /dev/input/event* permissions.\
Possible device names are:\n %s.\n \
Possible device descriptors are %s:\n %s',
self.device_id,
device_names,
id_folder,
os.listdir(id_folder)
)
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.stopped = threading.Event() self.stopped = threading.Event()
self.hass = hass self.hass = hass
self.key_value = key_value self.key_value = KEY_VALUE.get(config.get(TYPE, 'key_up'))
def _get_keyboard_device(self):
from evdev import InputDevice, list_devices
if self.device_name:
devices = [InputDevice(file_name) for file_name in list_devices()]
for device in devices:
if self.device_name == device.name:
return device
elif self.device_descriptor:
try:
device = InputDevice(self.device_descriptor)
except OSError:
pass
else:
return device
return None
def run(self): def run(self):
"""Main loop of the KeyboardRemote.""" """Main loop of the KeyboardRemote."""
from evdev import categorize, ecodes, InputDevice from evdev import categorize, ecodes
if self.keyboard_connected: if self.dev is not None:
self.dev.grab() self.dev.grab()
_LOGGER.debug( _LOGGER.debug(
'KeyboardRemote interface started for %s', 'Interface started for %s',
self.dev) self.dev)
while not self.stopped.isSet(): while not self.stopped.isSet():
# Sleeps to ease load on processor # Sleeps to ease load on processor
time.sleep(.1) time.sleep(.1)
if not self.keyboard_connected: if self.dev is None:
try: self.dev = self._get_keyboard_device()
self.dev = InputDevice(self.device_descriptor) if self.dev is not None:
except OSError: # still disconnected
continue
else:
self.dev.grab() self.dev.grab()
self.keyboard_connected = True self.hass.bus.fire(
_LOGGER.debug('KeyboardRemote: keyboard re-connected, %s', KEYBOARD_REMOTE_CONNECTED
self.device_descriptor) )
_LOGGER.debug('Keyboard re-connected, %s',
self.device_id)
else:
continue
try: try:
event = self.dev.read_one() event = self.dev.read_one()
except IOError: # Keyboard Disconnected except IOError: # Keyboard Disconnected
self.keyboard_connected = False self.dev = None
_LOGGER.debug('KeyboardRemote: keyard disconnected, %s', self.hass.bus.fire(
self.device_descriptor) KEYBOARD_REMOTE_DISCONNECTED
)
_LOGGER.debug('Keyboard disconnected, %s',
self.device_id)
continue continue
if not event: if not event:

View File

@ -0,0 +1,118 @@
"""
Support for Avion dimmers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.avion/
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light,
PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['avion==0.5']
_LOGGER = logging.getLogger(__name__)
SUPPORT_AVION_LED = (SUPPORT_BRIGHTNESS)
DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA},
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Avion switch."""
lights = []
for address, device_config in config[CONF_DEVICES].items():
device = {}
device['name'] = device_config[CONF_NAME]
device['key'] = device_config[CONF_API_KEY]
device['address'] = address
light = AvionLight(device)
if light.is_valid:
lights.append(light)
add_devices(lights)
class AvionLight(Light):
"""Representation of an Avion light."""
def __init__(self, device):
"""Initialize the light."""
# pylint: disable=import-error
import avion
self._name = device['name']
self._address = device['address']
self._key = device['key']
self._brightness = 255
self._state = False
self._switch = avion.avion(self._address, self._key)
self._switch.connect()
self.is_valid = True
@property
def unique_id(self):
"""Return the ID of this light."""
return "{}.{}".format(self.__class__, self._address)
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._state
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_AVION_LED
@property
def should_poll(self):
"""Don't poll."""
return False
@property
def assumed_state(self):
"""We can't read the actual state, so assume it matches."""
return True
def set_state(self, brightness):
"""Set the state of this lamp to the provided brightness."""
self._switch.set_brightness(brightness)
return True
def turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None:
self._brightness = brightness
self.set_state(self.brightness)
self._state = True
def turn_off(self, **kwargs):
"""Turn the specified or all lights off."""
self.set_state(0)
self._state = False

View File

@ -0,0 +1,124 @@
"""
Support for Decora dimmers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.decora/
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light,
PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['decora==0.3']
_LOGGER = logging.getLogger(__name__)
SUPPORT_DECORA_LED = (SUPPORT_BRIGHTNESS)
DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA},
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Decora switch."""
lights = []
for address, device_config in config[CONF_DEVICES].items():
device = {}
device['name'] = device_config[CONF_NAME]
device['key'] = device_config[CONF_API_KEY]
device['address'] = address
light = DecoraLight(device)
if light.is_valid:
lights.append(light)
add_devices(lights)
class DecoraLight(Light):
"""Representation of an Decora light."""
def __init__(self, device):
"""Initialize the light."""
# pylint: disable=import-error
import decora
self._name = device['name']
self._address = device['address']
self._key = device["key"]
self._switch = decora.decora(self._address, self._key)
self._switch.connect()
self._state = self._switch.get_on()
self._brightness = self._switch.get_brightness()
self.is_valid = True
@property
def unique_id(self):
"""Return the ID of this light."""
return "{}.{}".format(self.__class__, self._address)
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._state
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_DECORA_LED
@property
def should_poll(self):
"""We can read the device state, so poll."""
return True
@property
def assumed_state(self):
"""We can read the actual state."""
return False
def set_state(self, brightness):
"""Set the state of this lamp to the provided brightness."""
self._switch.set_brightness(brightness)
self._brightness = brightness
return True
def turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
self._switch.on()
if brightness is not None:
self.set_state(brightness)
self._state = True
def turn_off(self, **kwargs):
"""Turn the specified or all lights off."""
self._switch.off()
self._state = False
def update(self):
"""Synchronise internal state with the actual light state."""
self._brightness = self._switch.get_brightness()
self._state = self._switch.get_on()

View File

@ -25,6 +25,7 @@ from homeassistant.components.light import (
from homeassistant.config import load_yaml_config_file 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.loader import get_component from homeassistant.loader import get_component
from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['phue==0.9'] REQUIREMENTS = ['phue==0.9']
@ -50,10 +51,21 @@ SUPPORT_HUE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION |
SUPPORT_XY_COLOR) SUPPORT_XY_COLOR)
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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, vol.Optional(CONF_ALLOW_UNREACHABLE,
vol.Optional(CONF_FILENAME): cv.string, default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
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,
}) })
ATTR_GROUP_NAME = "group_name" ATTR_GROUP_NAME = "group_name"
@ -63,6 +75,8 @@ SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_SCENE_NAME): cv.string, vol.Required(ATTR_SCENE_NAME): cv.string,
}) })
ATTR_IS_HUE_GROUP = "is_hue_group"
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
"""Attempt to detect host based on existing configuration.""" """Attempt to detect host based on existing configuration."""
@ -84,9 +98,10 @@ def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Hue lights.""" """Setup the Hue lights."""
# Default needed in case of discovery # Default needed in case of discovery
filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE) filename = config.get(CONF_FILENAME)
allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE, allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE)
DEFAULT_ALLOW_UNREACHABLE) allow_in_emulated_hue = config.get(CONF_ALLOW_IN_EMULATED_HUE)
allow_hue_groups = config.get(CONF_ALLOW_HUE_GROUPS)
if discovery_info is not None: if discovery_info is not None:
host = urlparse(discovery_info[1]).hostname host = urlparse(discovery_info[1]).hostname
@ -109,10 +124,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
socket.gethostbyname(host) in _CONFIGURED_BRIDGES: socket.gethostbyname(host) in _CONFIGURED_BRIDGES:
return return
setup_bridge(host, hass, add_devices, filename, allow_unreachable) setup_bridge(host, hass, add_devices, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
def setup_bridge(host, hass, add_devices, filename, allow_unreachable): def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups):
"""Setup a phue bridge based on host parameter.""" """Setup a phue bridge based on host parameter."""
import phue import phue
@ -129,7 +146,8 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
_LOGGER.warning("Connected to Hue at %s but not registered.", host) _LOGGER.warning("Connected to Hue at %s but not registered.", host)
request_configuration(host, hass, add_devices, filename, request_configuration(host, hass, add_devices, filename,
allow_unreachable) allow_unreachable, allow_in_emulated_hue,
allow_hue_groups)
return return
@ -143,7 +161,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
lights = {} lights = {}
lightgroups = {} lightgroups = {}
skip_groups = False skip_groups = not allow_hue_groups
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights(): def update_lights():
@ -184,7 +202,8 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
if light_id not in lights: if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info, lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights, bridge, update_lights,
bridge_type, allow_unreachable) bridge_type, allow_unreachable,
allow_in_emulated_hue)
new_lights.append(lights[light_id]) new_lights.append(lights[light_id])
else: else:
lights[light_id].info = info lights[light_id].info = info
@ -200,7 +219,8 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
if lightgroup_id not in lightgroups: if lightgroup_id not in lightgroups:
lightgroups[lightgroup_id] = HueLight( lightgroups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge, update_lights, int(lightgroup_id), info, bridge, update_lights,
bridge_type, allow_unreachable, True) bridge_type, allow_unreachable, allow_in_emulated_hue,
True)
new_lights.append(lightgroups[lightgroup_id]) new_lights.append(lightgroups[lightgroup_id])
else: else:
lightgroups[lightgroup_id].info = info lightgroups[lightgroup_id].info = info
@ -229,7 +249,8 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
def request_configuration(host, hass, add_devices, filename, def request_configuration(host, hass, add_devices, filename,
allow_unreachable): allow_unreachable, allow_in_emulated_hue,
allow_hue_groups):
"""Request configuration steps from the user.""" """Request configuration steps from the user."""
configurator = get_component('configurator') configurator = get_component('configurator')
@ -243,7 +264,8 @@ def request_configuration(host, hass, add_devices, filename,
# pylint: disable=unused-argument # pylint: disable=unused-argument
def hue_configuration_callback(data): def hue_configuration_callback(data):
"""The actions to do when our configuration callback is called.""" """The actions to do when our configuration callback is called."""
setup_bridge(host, hass, add_devices, filename, allow_unreachable) setup_bridge(host, hass, add_devices, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
_CONFIGURING[host] = configurator.request_config( _CONFIGURING[host] = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback, hass, "Philips Hue", hue_configuration_callback,
@ -259,7 +281,8 @@ class HueLight(Light):
"""Representation of a Hue light.""" """Representation of a Hue light."""
def __init__(self, light_id, info, bridge, update_lights, def __init__(self, light_id, info, bridge, update_lights,
bridge_type, allow_unreachable, is_group=False): bridge_type, allow_unreachable, allow_in_emulated_hue,
is_group=False):
"""Initialize the light.""" """Initialize the light."""
self.light_id = light_id self.light_id = light_id
self.info = info self.info = info
@ -268,6 +291,7 @@ class HueLight(Light):
self.bridge_type = bridge_type self.bridge_type = bridge_type
self.allow_unreachable = allow_unreachable self.allow_unreachable = allow_unreachable
self.is_group = is_group self.is_group = is_group
self.allow_in_emulated_hue = allow_in_emulated_hue
if is_group: if is_group:
self._command_func = self.bridge.set_group self._command_func = self.bridge.set_group
@ -395,3 +419,13 @@ class HueLight(Light):
def update(self): def update(self):
"""Synchronize state with bridge.""" """Synchronize state with bridge."""
self.update_lights(no_throttle=True) self.update_lights(no_throttle=True)
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
if not self.allow_in_emulated_hue:
attributes[ATTR_EMULATED_HUE] = self.allow_in_emulated_hue
if self.is_group:
attributes[ATTR_IS_HUE_GROUP] = self.is_group
return attributes

View File

@ -38,15 +38,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
setup_light(device_id, conf_lights[device_id], insteonhub, hass, setup_light(device_id, conf_lights[device_id], insteonhub, hass,
add_devices) add_devices)
linked = insteonhub.get_linked() else:
linked = insteonhub.get_linked()
for device_id in linked: for device_id in linked:
if (linked[device_id]['cat_type'] == 'dimmer' and if (linked[device_id]['cat_type'] == 'dimmer' and
device_id not in conf_lights): device_id not in conf_lights):
request_configuration(device_id, request_configuration(device_id,
insteonhub, insteonhub,
linked[device_id]['model_name'] + ' ' + linked[device_id]['model_name'] + ' ' +
linked[device_id]['sku'], hass, add_devices) linked[device_id]['sku'],
hass, add_devices)
def request_configuration(device_id, insteonhub, model, hass, def request_configuration(device_id, insteonhub, model, hass,

View File

@ -8,18 +8,13 @@ import logging
from typing import Callable from typing import Callable
from homeassistant.components.light import ( from homeassistant.components.light import (
Light, SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS) Light, SUPPORT_BRIGHTNESS)
import homeassistant.components.isy994 as isy import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
False: STATE_OFF,
True: STATE_ON,
}
UOM = ['2', '51', '78'] UOM = ['2', '51', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false', '%'] STATES = [STATE_OFF, STATE_ON, 'true', 'false', '%']
@ -52,12 +47,12 @@ class ISYLightDevice(isy.ISYDevice, Light):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get whether the ISY994 light is on.""" """Get whether the ISY994 light is on."""
return self.state == STATE_ON return self.value > 0
@property @property
def state(self) -> str: def brightness(self) -> float:
"""Get the state of the ISY994 light.""" """Get the brightness of the ISY994 light."""
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN) return self.value
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device.""" """Send the turn off command to the ISY994 light device."""
@ -69,11 +64,6 @@ class ISYLightDevice(isy.ISYDevice, Light):
if not self._node.on(val=brightness): if not self._node.on(val=brightness):
_LOGGER.debug('Unable to turn on light.') _LOGGER.debug('Unable to turn on light.')
@property
def state_attributes(self):
"""Flag supported attributes."""
return {ATTR_BRIGHTNESS: self.value}
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""

View File

@ -0,0 +1,98 @@
"""Support for Lutron lights."""
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS, DOMAIN, SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.lutron import (
LutronDevice, LUTRON_DEVICES, LUTRON_GROUPS, LUTRON_CONTROLLER)
DEPENDENCIES = ['lutron']
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Lutron lights."""
area_devs = {}
devs = []
for (area_name, device) in hass.data[LUTRON_DEVICES]['light']:
dev = LutronLight(hass, area_name, device,
hass.data[LUTRON_CONTROLLER])
area_devs.setdefault(area_name, []).append(dev)
devs.append(dev)
add_devices(devs, True)
for area in area_devs:
if area not in hass.data[LUTRON_GROUPS]:
continue
grp = hass.data[LUTRON_GROUPS][area]
ids = list(grp.tracking) + [dev.entity_id for dev in area_devs[area]]
grp.update_tracked_entity_ids(ids)
return True
def to_lutron_level(level):
"""Convert the given HASS light level (0-255) to Lutron (0.0-100.0)."""
return float((level * 100) / 255)
def to_hass_level(level):
"""Convert the given Lutron (0.0-100.0) light level to HASS (0-255)."""
return int((level * 255) / 100)
class LutronLight(LutronDevice, Light):
"""Representation of a Lutron Light, including dimmable."""
def __init__(self, hass, area_name, lutron_device, controller):
"""Initialize the light."""
self._prev_brightness = None
LutronDevice.__init__(self, hass, DOMAIN, area_name, lutron_device,
controller)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
@property
def brightness(self):
"""Return the brightness of the light."""
new_brightness = to_hass_level(self._lutron_device.last_level())
if new_brightness != 0:
self._prev_brightness = new_brightness
return new_brightness
def turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs and self._lutron_device.is_dimmable:
brightness = kwargs[ATTR_BRIGHTNESS]
elif self._prev_brightness == 0:
brightness = 255 / 2
else:
brightness = self._prev_brightness
self._prev_brightness = brightness
self._lutron_device.level = to_lutron_level(brightness)
def turn_off(self, **kwargs):
"""Turn the light off."""
self._lutron_device.level = 0
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
attr['Lutron Integration ID'] = self._lutron_device.id
return attr
@property
def is_on(self):
"""Return true if device is on."""
return self._lutron_device.last_level() > 0
def update(self):
"""Called when forcing a refresh of the device."""
if self._prev_brightness is None:
self._prev_brightness = to_hass_level(self._lutron_device.level)

View File

@ -0,0 +1,104 @@
"""
Support for Piglow LED's.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.piglow/
"""
import logging
import subprocess
import voluptuous as vol
# Import the device class from the component that you want to support
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS,
ATTR_RGB_COLOR, SUPPORT_RGB_COLOR,
Light, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
# Home Assistant depends on 3rd party packages for API specific code.
REQUIREMENTS = ['piglow==1.2.4']
_LOGGER = logging.getLogger(__name__)
SUPPORT_PIGLOW = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
DEFAULT_NAME = 'Piglow'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Piglow Light platform."""
import piglow
if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != '54':
_LOGGER.error("A Piglow device was not found")
return False
name = config.get(CONF_NAME)
# Add devices
add_devices([PiglowLight(piglow, name)])
class PiglowLight(Light):
"""Representation of an Piglow Light."""
def __init__(self, piglow, name):
"""Initialize an PiglowLight."""
self._piglow = piglow
self._name = name
self._is_on = False
self._brightness = 255
self._rgb_color = [255, 255, 255]
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
return self._brightness
@property
def rgb_color(self):
"""Read back the color of the light."""
return self._rgb_color
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_PIGLOW
@property
def is_on(self):
"""Return true if light is on."""
return self._is_on
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
self._piglow.clear()
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
percent_bright = (self._brightness / 255)
if ATTR_RGB_COLOR in kwargs:
self._rgb_color = kwargs[ATTR_RGB_COLOR]
self._piglow.red(int(self._rgb_color[0] * percent_bright))
self._piglow.green(int(self._rgb_color[1] * percent_bright))
self._piglow.blue(int(self._rgb_color[2] * percent_bright))
else:
self._piglow.all(self._brightness)
self._piglow.show()
self._is_on = True
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
self._piglow.clear()
self._piglow.show()
self._is_on = False

View File

@ -5,8 +5,11 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/ https://home-assistant.io/components/light.qwikswitch/
""" """
import logging import logging
import homeassistant.components.qwikswitch as qwikswitch import homeassistant.components.qwikswitch as qwikswitch
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['qwikswitch'] DEPENDENCIES = ['qwikswitch']
@ -14,7 +17,7 @@ DEPENDENCIES = ['qwikswitch']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Add lights from the main Qwikswitch component.""" """Add lights from the main Qwikswitch component."""
if discovery_info is None: if discovery_info is None:
logging.getLogger(__name__).error('Configure Qwikswitch Component.') _LOGGER.error("Configure Qwikswitch component")
return False return False
add_devices(qwikswitch.QSUSB['light']) add_devices(qwikswitch.QSUSB['light'])

View File

@ -83,7 +83,9 @@ class TellstickLight(TellstickDevice, Light):
def _send_device_command(self, requested_state, requested_data): def _send_device_command(self, requested_state, requested_data):
"""Let tellcore update the actual device to the requested state.""" """Let tellcore update the actual device to the requested state."""
if requested_state: if requested_state:
brightness = requested_data if requested_data is not None:
self._tellcore_device.dim(brightness) self._brightness = int(requested_data)
self._tellcore_device.dim(self._brightness)
else: else:
self._tellcore_device.turn_off() self._tellcore_device.turn_off()

View File

@ -23,7 +23,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink lights.""" """Setup the Wink lights."""
import pywink import pywink
add_devices(WinkLight(light, hass) for light in pywink.get_bulbs()) add_devices(WinkLight(light, hass) for light in pywink.get_light_bulbs())
class WinkLight(WinkDevice, Light): class WinkLight(WinkDevice, Light):

View File

@ -33,16 +33,10 @@ def x10_command(command):
return check_output(['heyu'] + command.split(' '), stderr=STDOUT) return check_output(['heyu'] + command.split(' '), stderr=STDOUT)
def get_status():
"""Get on/off status for all x10 units in default housecode."""
output = check_output('heyu info | grep monitored', shell=True)
return output.decode('utf-8').split(' ')[-1].strip('\n()')
def get_unit_status(code): def get_unit_status(code):
"""Get on/off status for given unit.""" """Get on/off status for given unit."""
unit = int(code[1:]) output = check_output('heyu onstate ' + code, shell=True)
return get_status()[16 - int(unit)] == '1' return int(output.decode('utf-8')[0])
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -63,8 +57,8 @@ class X10Light(Light):
"""Initialize an X10 Light.""" """Initialize an X10 Light."""
self._name = light['name'] self._name = light['name']
self._id = light['id'] self._id = light['id']
self._is_on = False
self._brightness = 0 self._brightness = 0
self._state = False
@property @property
def name(self): def name(self):
@ -79,7 +73,7 @@ class X10Light(Light):
@property @property
def is_on(self): def is_on(self):
"""Return true if light is on.""" """Return true if light is on."""
return self._is_on return self._state
@property @property
def supported_features(self): def supported_features(self):
@ -90,13 +84,13 @@ class X10Light(Light):
"""Instruct the light to turn on.""" """Instruct the light to turn on."""
x10_command('on ' + self._id) x10_command('on ' + self._id)
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
self._is_on = True self._state = True
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Instruct the light to turn off.""" """Instruct the light to turn off."""
x10_command('off ' + self._id) x10_command('off ' + self._id)
self._is_on = False self._state = False
def update(self): def update(self):
"""Fetch new state data for this light.""" """Fetch update state."""
self._is_on = get_unit_status(self._id) self._state = bool(get_unit_status(self._id))

View File

@ -17,6 +17,7 @@ from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \ from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
color_temperature_mired_to_kelvin, color_temperature_to_rgb, \ color_temperature_mired_to_kelvin, color_temperature_to_rgb, \
color_rgb_to_rgbw, color_rgbw_to_rgb color_rgb_to_rgbw, color_rgbw_to_rgb
from homeassistant.helpers import customize
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -42,7 +43,10 @@ TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN
SUPPORT_ZWAVE = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR SUPPORT_ZWAVE_DIMMER = SUPPORT_BRIGHTNESS
SUPPORT_ZWAVE_COLOR = SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
| SUPPORT_COLOR_TEMP)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -51,13 +55,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
customize = hass.data['zwave_customize']
name = '{}.{}'.format(DOMAIN, zwave.object_id(value)) name = '{}.{}'.format(DOMAIN, zwave.object_id(value))
node_config = customize.get(name, {}) node_config = customize.get_overrides(hass, zwave.DOMAIN, name)
refresh = node_config.get(zwave.CONF_REFRESH_VALUE) refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
delay = node_config.get(zwave.CONF_REFRESH_DELAY) delay = node_config.get(zwave.CONF_REFRESH_DELAY)
_LOGGER.debug('customize=%s name=%s node_config=%s CONF_REFRESH_VALUE=%s' _LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s'
' CONF_REFRESH_DELAY=%s', customize, name, node_config, ' CONF_REFRESH_DELAY=%s', name, node_config,
refresh, delay) refresh, delay)
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL: if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
return return
@ -87,9 +90,6 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
def __init__(self, value, refresh, delay): def __init__(self, value, refresh, delay):
"""Initialize the light.""" """Initialize the light."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._brightness = None self._brightness = None
self._state = None self._state = None
@ -115,38 +115,33 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
self._timer = None self._timer = None
_LOGGER.debug('self._refreshing=%s self.delay=%s', _LOGGER.debug('self._refreshing=%s self.delay=%s',
self._refresh_value, self._delay) self._refresh_value, self._delay)
dispatcher.connect(
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
def update_properties(self): def update_properties(self):
"""Update internal properties based on zwave values.""" """Update internal properties based on zwave values."""
# Brightness # Brightness
self._brightness, self._state = brightness_state(self._value) self._brightness, self._state = brightness_state(self._value)
def _value_changed(self, value): def value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value for this entity's node has changed."""
if self._value.value_id == value.value_id or \ if self._refresh_value:
self._value.node == value.node: if self._refreshing:
_LOGGER.debug('Value changed for label %s', self._value.label) self._refreshing = False
if self._refresh_value:
if self._refreshing:
self._refreshing = False
self.update_properties()
else:
def _refresh_value():
"""Used timer callback for delayed value refresh."""
self._refreshing = True
self._value.refresh()
if self._timer is not None and self._timer.isAlive():
self._timer.cancel()
self._timer = Timer(self._delay, _refresh_value)
self._timer.start()
self.schedule_update_ha_state()
else:
self.update_properties() self.update_properties()
self.schedule_update_ha_state() else:
def _refresh_value():
"""Used timer callback for delayed value refresh."""
self._refreshing = True
self._value.refresh()
if self._timer is not None and self._timer.isAlive():
self._timer.cancel()
self._timer = Timer(self._delay, _refresh_value)
self._timer.start()
self.schedule_update_ha_state()
else:
self.update_properties()
self.schedule_update_ha_state()
@property @property
def brightness(self): def brightness(self):
@ -161,7 +156,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_ZWAVE return SUPPORT_ZWAVE_DIMMER
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
@ -351,3 +346,11 @@ class ZwaveColorLight(ZwaveDimmer):
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw) self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
super().turn_on(**kwargs) super().turn_on(**kwargs)
@property
def supported_features(self):
"""Flag supported features."""
if self._zw098:
return SUPPORT_ZWAVE_COLORTEMP
else:
return SUPPORT_ZWAVE_COLOR

View File

@ -1,21 +1,57 @@
lock: clear_usercode:
description: Lock all or specified locks description: Clear a usercode from lock
fields: fields:
entity_id: node_id:
description: Name of lock to lock description: Node id of the lock
example: 'lock.front_door' example: 18
code: code_slot:
description: An optional code to lock the lock with description: Code slot to clear code from
example: 1234 example: 1
unlock: get_usercode:
description: Unlock all or specified locks description: Retrieve a usercode from lock
fields: fields:
entity_id: node_id:
description: Name of lock to unlock description: Node id of the lock
example: 'lock.front_door' example: 18
code: code_slot:
description: An optional code to unlock the lock with description: Code slot to retrive a code from
example: 1234 example: 1
lock:
description: Lock all or specified locks
fields:
entity_id:
description: Name of lock to lock
example: 'lock.front_door'
code:
description: An optional code to lock the lock with
example: 1234
set_usercode:
description: Set a usercode to lock
fields:
node_id:
description: Node id of the lock
example: 18
code_slot:
description: Code slot to set the code
example: 1
usercode:
description: Code to set
example: 1234
unlock:
description: Unlock all or specified locks
fields:
entity_id:
description: Name of lock to unlock
example: 'lock.front_door'
code:
description: An optional code to unlock the lock with
example: 1234

View File

@ -7,13 +7,24 @@ https://home-assistant.io/components/lock.zwave/
# Because we do not compile openzwave on CI # Because we do not compile openzwave on CI
# pylint: disable=import-error # pylint: disable=import-error
import logging import logging
from os import path
import voluptuous as vol
from homeassistant.components.lock import DOMAIN, LockDevice from homeassistant.components.lock import DOMAIN, LockDevice
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.config import load_yaml_config_file
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_NOTIFICATION = 'notification' ATTR_NOTIFICATION = 'notification'
ATTR_LOCK_STATUS = 'lock_status'
ATTR_CODE_SLOT = 'code_slot'
ATTR_USERCODE = 'usercode'
SERVICE_SET_USERCODE = 'set_usercode'
SERVICE_GET_USERCODE = 'get_usercode'
SERVICE_CLEAR_USERCODE = 'clear_usercode'
LOCK_NOTIFICATION = { LOCK_NOTIFICATION = {
1: 'Manual Lock', 1: 'Manual Lock',
@ -22,18 +33,80 @@ LOCK_NOTIFICATION = {
4: 'RF Unlock', 4: 'RF Unlock',
5: 'Keypad Lock', 5: 'Keypad Lock',
6: 'Keypad Unlock', 6: 'Keypad Unlock',
11: 'Lock Jammed',
254: 'Unknown Event' 254: 'Unknown Event'
} }
LOCK_ALARM_TYPE = {
9: 'Deadbolt Jammed',
18: 'Locked with Keypad by user',
19: 'Unlocked with Keypad by user ',
21: 'Manually Locked by',
22: 'Manually Unlocked by Key or Inside thumb turn',
24: 'Locked by RF',
25: 'Unlocked by RF',
27: 'Auto re-lock',
33: 'User deleted: ',
112: 'Master code changed or User added: ',
113: 'Duplicate Pin-code: ',
130: 'RF module, power restored',
161: 'Tamper Alarm: ',
167: 'Low Battery',
168: 'Critical Battery Level',
169: 'Battery too low to operate'
}
MANUAL_LOCK_ALARM_LEVEL = {
1: 'Key Cylinder or Inside thumb turn',
2: 'Touch function (lock and leave)'
}
TAMPER_ALARM_LEVEL = {
1: 'Too many keypresses',
2: 'Cover removed'
}
LOCK_STATUS = { LOCK_STATUS = {
1: True, 1: True,
2: False, 2: False,
3: True, 3: True,
4: False, 4: False,
5: True, 5: True,
6: False 6: False,
9: False,
18: True,
19: False,
21: True,
22: False,
24: True,
25: False,
27: True
} }
ALARM_TYPE_STD = [
18,
19,
33,
112,
113
]
SET_USERCODE_SCHEMA = vol.Schema({
vol.Required(zwave.const.ATTR_NODE_ID): vol.Coerce(int),
vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
vol.Required(ATTR_USERCODE): vol.Coerce(int),
})
GET_USERCODE_SCHEMA = vol.Schema({
vol.Required(zwave.const.ATTR_NODE_ID): vol.Coerce(int),
vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
})
CLEAR_USERCODE_SCHEMA = vol.Schema({
vol.Required(zwave.const.ATTR_NODE_ID): vol.Coerce(int),
vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -44,13 +117,81 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
def set_usercode(service):
"""Set the usercode to index X on the lock."""
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
lock_node = zwave.NETWORK.nodes[node_id]
code_slot = service.data.get(ATTR_CODE_SLOT)
usercode = service.data.get(ATTR_USERCODE)
for value in lock_node.get_values(
class_id=zwave.const.COMMAND_CLASS_USER_CODE).values():
if value.index != code_slot:
continue
if len(str(usercode)) > 4:
_LOGGER.error('Invalid code provided: (%s)'
' usercode must %s or less digits',
usercode, len(value.data))
value.data = str(usercode)
break
def get_usercode(service):
"""Get a usercode at index X on the lock."""
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
lock_node = zwave.NETWORK.nodes[node_id]
code_slot = service.data.get(ATTR_CODE_SLOT)
for value in lock_node.get_values(
class_id=zwave.const.COMMAND_CLASS_USER_CODE).values():
if value.index != code_slot:
continue
_LOGGER.info('Usercode at slot %s is: %s', value.index, value.data)
break
def clear_usercode(service):
"""Set usercode to slot X on the lock."""
node_id = service.data.get(zwave.const.ATTR_NODE_ID)
lock_node = zwave.NETWORK.nodes[node_id]
code_slot = service.data.get(ATTR_CODE_SLOT)
data = ''
for value in lock_node.get_values(
class_id=zwave.const.COMMAND_CLASS_USER_CODE).values():
if value.index != code_slot:
continue
for i in range(len(value.data)):
data += '\0'
i += 1
_LOGGER.debug('Data to clear lock: %s', data)
value.data = data
_LOGGER.info('Usercode at slot %s is cleared', value.index)
break
if value.command_class != zwave.const.COMMAND_CLASS_DOOR_LOCK: if value.command_class != zwave.const.COMMAND_CLASS_DOOR_LOCK:
return return
if value.type != zwave.const.TYPE_BOOL: if value.type != zwave.const.TYPE_BOOL:
return return
if value.genre != zwave.const.GENRE_USER: if value.genre != zwave.const.GENRE_USER:
return return
if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE):
hass.services.register(DOMAIN,
SERVICE_SET_USERCODE,
set_usercode,
descriptions.get(SERVICE_SET_USERCODE),
schema=SET_USERCODE_SCHEMA)
hass.services.register(DOMAIN,
SERVICE_GET_USERCODE,
get_usercode,
descriptions.get(SERVICE_GET_USERCODE),
schema=GET_USERCODE_SCHEMA)
hass.services.register(DOMAIN,
SERVICE_CLEAR_USERCODE,
clear_usercode,
descriptions.get(SERVICE_CLEAR_USERCODE),
schema=CLEAR_USERCODE_SCHEMA)
value.set_change_verified(False) value.set_change_verified(False)
add_devices([ZwaveLock(value)]) add_devices([ZwaveLock(value)])
@ -60,28 +201,16 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
def __init__(self, value): def __init__(self, value):
"""Initialize the Z-Wave switch device.""" """Initialize the Z-Wave switch device."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node self._node = value.node
self._state = None self._state = None
self._notification = None self._notification = None
dispatcher.connect( self._lock_status = None
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
self.update_properties() self.update_properties()
def _value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
_LOGGER.debug('Value changed for label %s', self._value.label)
self.update_properties()
self.schedule_update_ha_state()
def update_properties(self): def update_properties(self):
"""Callback on data change for the registered node/value pair.""" """Callback on data changes for node values."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_ALARM).values(): class_id=zwave.const.COMMAND_CLASS_ALARM).values():
if value.label != "Access Control": if value.label != "Access Control":
@ -89,9 +218,55 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
self._notification = LOCK_NOTIFICATION.get(value.data) self._notification = LOCK_NOTIFICATION.get(value.data)
if self._notification: if self._notification:
self._state = LOCK_STATUS.get(value.data) self._state = LOCK_STATUS.get(value.data)
_LOGGER.debug('Lock state set from Access Control value and'
' is %s', value.data)
break break
if not self._notification:
self._state = self._value.data for value in self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_ALARM).values():
if value.label != "Alarm Type":
continue
alarm_type = LOCK_ALARM_TYPE.get(value.data)
if alarm_type:
self._state = LOCK_STATUS.get(value.data)
_LOGGER.debug('Lock state set from Alarm Type value and'
' is %s', value.data)
break
for value in self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_ALARM).values():
if value.label != "Alarm Level":
continue
alarm_level = value.data
_LOGGER.debug('Lock alarm_level is %s', alarm_level)
if alarm_type is 21:
self._lock_status = '{}{}'.format(
LOCK_ALARM_TYPE.get(alarm_type),
MANUAL_LOCK_ALARM_LEVEL.get(alarm_level))
if alarm_type in ALARM_TYPE_STD:
self._lock_status = '{}{}'.format(
LOCK_ALARM_TYPE.get(alarm_type), alarm_level)
break
if alarm_type is 161:
self._lock_status = '{}{}'.format(
LOCK_ALARM_TYPE.get(alarm_type),
TAMPER_ALARM_LEVEL.get(alarm_level))
break
if alarm_type != 0:
self._lock_status = LOCK_ALARM_TYPE.get(alarm_type)
break
if not self._notification and not self._lock_status:
for value in self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_DOOR_LOCK).values():
if value.type != zwave.const.TYPE_BOOL:
continue
if value.genre != zwave.const.GENRE_USER:
continue
self._state = value.data
_LOGGER.debug('Lock state set from Bool value and'
' is %s', value.data)
break
@property @property
def is_locked(self): def is_locked(self):
@ -112,4 +287,6 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
data = super().device_state_attributes data = super().device_state_attributes
if self._notification: if self._notification:
data[ATTR_NOTIFICATION] = self._notification data[ATTR_NOTIFICATION] = self._notification
if self._lock_status:
data[ATTR_LOCK_STATUS] = self._lock_status
return data return data

View File

@ -307,6 +307,10 @@ def _exclude_events(events, config):
if event.event_type == EVENT_STATE_CHANGED: if event.event_type == EVENT_STATE_CHANGED:
to_state = State.from_dict(event.data.get('new_state')) to_state = State.from_dict(event.data.get('new_state'))
# Do not report on new entities # Do not report on new entities
if event.data.get('old_state') is None:
continue
# Do not report on entity removal
if not to_state: if not to_state:
continue continue

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