mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
commit
46aa2e7ce1
23
.coveragerc
23
.coveragerc
@ -46,6 +46,9 @@ omit =
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
@ -122,6 +125,9 @@ omit =
|
||||
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/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
@ -130,6 +136,7 @@ omit =
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/flic.py
|
||||
homeassistant/components/binary_sensor/hikvision.py
|
||||
homeassistant/components/binary_sensor/iss.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
@ -160,14 +167,17 @@ omit =
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/linksys_ap.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/ping.py
|
||||
homeassistant/components/device_tracker/sky_hub.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tado.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
@ -185,7 +195,9 @@ omit =
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/light/avion.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
@ -195,8 +207,10 @@ omit =
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
@ -208,6 +222,7 @@ omit =
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/hdmi_cec.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
@ -231,6 +246,7 @@ omit =
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
@ -256,12 +272,13 @@ omit =
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/openalpr.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@ -293,7 +310,6 @@ omit =
|
||||
homeassistant/components/sensor/hddtemp.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/hydroquebec.py
|
||||
homeassistant/components/sensor/iss.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
@ -318,6 +334,7 @@ omit =
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
homeassistant/components/sensor/sma.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
@ -348,6 +365,7 @@ omit =
|
||||
homeassistant/components/switch/digitalloggers.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hdmi_cec.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/kankun.py
|
||||
@ -362,6 +380,7 @@ omit =
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
|
6
.ignore
Normal file
6
.ignore
Normal 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
|
11
.travis.yml
11
.travis.yml
@ -8,15 +8,16 @@ matrix:
|
||||
env: TOXENV=requirements
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
allow_failures:
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
|
39
CLA.md
Normal file
39
CLA.md
Normal 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
80
CODE_OF_CONDUCT.md
Normal 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/
|
@ -7,7 +7,7 @@ RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# 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
|
||||
|
||||
# Install hass component dependencies
|
||||
|
20
LICENSE
20
LICENSE
@ -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
194
LICENSE.md
Normal file
@ -0,0 +1,194 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
@ -87,7 +87,7 @@ components <https://home-assistant.io/developers/creating_components/>`__.
|
||||
|
||||
If you run into issues while using Home Assistant or during development
|
||||
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
|
||||
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||
|
@ -508,8 +508,10 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
Async friendly.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s%(reset)s")
|
||||
fmt = ("%(asctime)s %(levelname)s (%(threadName)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
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
@ -519,8 +521,8 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
fmt,
|
||||
datefmt='%y-%m-%d %H:%M:%S',
|
||||
colorfmt,
|
||||
datefmt=datefmt,
|
||||
reset=True,
|
||||
log_colors={
|
||||
'DEBUG': 'cyan',
|
||||
@ -554,9 +556,7 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
err_log_path, mode='w', delay=True)
|
||||
|
||||
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||
err_handler.setFormatter(
|
||||
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
||||
datefmt='%y-%m-%d %H:%M:%S'))
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
||||
|
||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||
hass.data[core.DATA_ASYNCHANDLER] = async_handler
|
||||
|
68
homeassistant/components/alarm_control_panel/wink.py
Normal file
68
homeassistant/components/alarm_control_panel/wink.py
Normal 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()
|
||||
}
|
@ -63,7 +63,7 @@ def read_input(pin):
|
||||
"""Read a value from a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
return GPIO.input(pin)
|
||||
return GPIO.input(pin) is GPIO.HIGH
|
||||
|
||||
|
||||
def edge_detect(pin, event_callback, bounce):
|
||||
|
@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
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)
|
||||
pin = config.get(CONF_PIN)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
@ -38,13 +38,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
try:
|
||||
response = requests.get(resource, timeout=10).json()
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error('Missing resource or schema in configuration. '
|
||||
'Add http:// to your URL.')
|
||||
_LOGGER.error("Missing resource or schema in configuration. "
|
||||
"Add http:// to your URL")
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('No route to device at %s. '
|
||||
'Please check the IP address in the configuration file.',
|
||||
resource)
|
||||
_LOGGER.error("No route to device at %s", resource)
|
||||
return False
|
||||
|
||||
arest = ArestData(resource, pin)
|
||||
@ -67,10 +65,10 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
self.update()
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get('{}/mode/{}/i'.format
|
||||
(self._resource, self._pin), timeout=10)
|
||||
request = requests.get(
|
||||
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
||||
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
|
||||
def name(self):
|
||||
@ -109,5 +107,4 @@ class ArestData(object):
|
||||
self._resource, self._pin), timeout=10)
|
||||
self.data = {'state': response.json()['return_value']}
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device '%s'. Is device offline?",
|
||||
self._resource)
|
||||
_LOGGER.error("No route to device '%s'", self._resource)
|
||||
|
89
homeassistant/components/binary_sensor/bbb_gpio.py
Normal file
89
homeassistant/components/binary_sensor/bbb_gpio.py
Normal 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
|
@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
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')
|
||||
droplets = config.get(CONF_DROPLETS)
|
||||
|
||||
@ -68,7 +68,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
return DEFAULT_SENSOR_CLASS
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the Digital Ocean droplet."""
|
||||
return {
|
||||
ATTR_CREATED_AT: self.data.created_at,
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/binary_sensor.ffmpeg/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from os import path
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -13,17 +14,22 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN)
|
||||
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.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, CONF_NAME,
|
||||
ATTR_ENTITY_ID)
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_START = 'ffmpeg_start'
|
||||
SERVICE_STOP = 'ffmpeg_stop'
|
||||
SERVICE_RESTART = 'ffmpeg_restart'
|
||||
|
||||
DATA_FFMPEG_DEVICE = 'ffmpeg_binary_sensor'
|
||||
|
||||
FFMPEG_SENSOR_NOISE = 'noise'
|
||||
FFMPEG_SENSOR_MOTION = 'motion'
|
||||
|
||||
@ -32,6 +38,7 @@ MAP_FFMPEG_BIN = [
|
||||
FFMPEG_SENSOR_MOTION
|
||||
]
|
||||
|
||||
CONF_INITIAL_STATE = 'initial_state'
|
||||
CONF_TOOL = 'tool'
|
||||
CONF_PEAK = 'peak'
|
||||
CONF_DURATION = 'duration'
|
||||
@ -41,10 +48,12 @@ CONF_REPEAT = 'repeat'
|
||||
CONF_REPEAT_TIME = 'repeat_time'
|
||||
|
||||
DEFAULT_NAME = 'FFmpeg'
|
||||
DEFAULT_INIT_STATE = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN),
|
||||
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_EXTRA_ARGUMENTS): 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)),
|
||||
})
|
||||
|
||||
SERVICE_RESTART_SCHEMA = vol.Schema({
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({
|
||||
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)
|
||||
|
||||
|
||||
# list of all ffmpeg sensors
|
||||
DEVICES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
from haffmpeg import SensorNoise, SensorMotion
|
||||
|
||||
# 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
|
||||
|
||||
# generate sensor object
|
||||
if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE:
|
||||
entity = FFmpegNoise(SensorNoise, config)
|
||||
entity = FFmpegNoise(hass, SensorNoise, config)
|
||||
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_entities([entity])
|
||||
DEVICES.append(entity)
|
||||
yield from async_add_devices([entity])
|
||||
|
||||
# exists service?
|
||||
if hass.services.has_service(DOMAIN, SERVICE_RESTART):
|
||||
hass.data[DATA_FFMPEG_DEVICE].append(entity)
|
||||
return
|
||||
hass.data[DATA_FFMPEG_DEVICE] = [entity]
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
# register service
|
||||
def _service_handle_restart(service):
|
||||
@asyncio.coroutine
|
||||
def async_service_handle(service):
|
||||
"""Handle service binary_sensor.ffmpeg_restart."""
|
||||
entity_ids = service.data.get('entity_id')
|
||||
|
||||
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]
|
||||
else:
|
||||
_devices = DEVICES
|
||||
_devices = hass.data[DATA_FFMPEG_DEVICE]
|
||||
|
||||
tasks = []
|
||||
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,
|
||||
_service_handle_restart,
|
||||
descriptions.get(SERVICE_RESTART),
|
||||
schema=SERVICE_RESTART_SCHEMA)
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
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):
|
||||
"""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."""
|
||||
self._manager = hass.data[DATA_FFMPEG]
|
||||
self._state = False
|
||||
self._config = config
|
||||
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 _callback(self, state):
|
||||
def _async_callback(self, state):
|
||||
"""HA-FFmpeg callback for noise detection."""
|
||||
self._state = state
|
||||
self.schedule_update_ha_state()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def _start_ffmpeg(self, config):
|
||||
"""Start a FFmpeg instance."""
|
||||
raise NotImplementedError
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
def shutdown_ffmpeg(self, event):
|
||||
"""For STOP event to shutdown ffmpeg."""
|
||||
self._ffmpeg.close()
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def restart_ffmpeg(self):
|
||||
"""Restart ffmpeg with new config."""
|
||||
self._ffmpeg.close()
|
||||
self._start_ffmpeg(self._config)
|
||||
def async_shutdown_ffmpeg(self):
|
||||
"""For STOP event to shutdown ffmpeg.
|
||||
|
||||
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
|
||||
def is_on(self):
|
||||
@ -177,20 +226,23 @@ class FFmpegBinarySensor(BinarySensorDevice):
|
||||
class FFmpegNoise(FFmpegBinarySensor):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def _start_ffmpeg(self, config):
|
||||
"""Start a FFmpeg instance."""
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
# init config
|
||||
self._ffmpeg.set_options(
|
||||
time_duration=config.get(CONF_DURATION),
|
||||
time_reset=config.get(CONF_RESET),
|
||||
peak=config.get(CONF_PEAK),
|
||||
time_duration=self._config.get(CONF_DURATION),
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
peak=self._config.get(CONF_PEAK),
|
||||
)
|
||||
|
||||
# run
|
||||
self._ffmpeg.open_sensor(
|
||||
input_source=config.get(CONF_INPUT),
|
||||
output_dest=config.get(CONF_OUTPUT),
|
||||
extra_cmd=config.get(CONF_EXTRA_ARGUMENTS),
|
||||
return self._ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
output_dest=self._config.get(CONF_OUTPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@property
|
||||
@ -202,20 +254,23 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
class FFmpegMotion(FFmpegBinarySensor):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def _start_ffmpeg(self, config):
|
||||
"""Start a FFmpeg instance."""
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
# init config
|
||||
self._ffmpeg.set_options(
|
||||
time_reset=config.get(CONF_RESET),
|
||||
time_repeat=config.get(CONF_REPEAT_TIME),
|
||||
repeat=config.get(CONF_REPEAT),
|
||||
changes=config.get(CONF_CHANGES),
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
time_repeat=self._config.get(CONF_REPEAT_TIME),
|
||||
repeat=self._config.get(CONF_REPEAT),
|
||||
changes=self._config.get(CONF_CHANGES),
|
||||
)
|
||||
|
||||
# run
|
||||
self._ffmpeg.open_sensor(
|
||||
input_source=config.get(CONF_INPUT),
|
||||
extra_cmd=config.get(CONF_EXTRA_ARGUMENTS),
|
||||
return self._ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -17,6 +17,7 @@ DEPENDENCIES = ['homematic']
|
||||
SENSOR_TYPES_CLASS = {
|
||||
"Remote": None,
|
||||
"ShutterContact": "opening",
|
||||
"MaxShutterContact": "opening",
|
||||
"IPShutterContact": "opening",
|
||||
"Smoke": "smoke",
|
||||
"SmokeV2": "smoke",
|
||||
|
@ -5,34 +5,39 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.iss/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
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
|
||||
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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ISS_VISIBLE = 'visible'
|
||||
ATTR_ISS_NEXT_RISE = 'next_rise'
|
||||
ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space'
|
||||
|
||||
CONF_SHOW_ON_MAP = 'show_on_map'
|
||||
|
||||
DEFAULT_NAME = 'ISS'
|
||||
DEFAULT_SENSOR_CLASS = 'visible'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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):
|
||||
"""Setup the ISS sensor."""
|
||||
# Validate the configuration
|
||||
"""Set up the ISS sensor."""
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
@ -45,75 +50,74 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return False
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
show_on_map = config.get(CONF_SHOW_ON_MAP)
|
||||
|
||||
sensors = []
|
||||
sensors.append(IssSensor(iss_data, name))
|
||||
|
||||
add_devices(sensors, True)
|
||||
add_devices([IssBinarySensor(iss_data, name, show_on_map)], True)
|
||||
|
||||
|
||||
class IssSensor(Entity):
|
||||
"""Implementation of a ISS sensor."""
|
||||
class IssBinarySensor(BinarySensorDevice):
|
||||
"""Implementation of the ISS binary sensor."""
|
||||
|
||||
def __init__(self, iss_data, name):
|
||||
def __init__(self, iss_data, name, show):
|
||||
"""Initialize the sensor."""
|
||||
self.iss_data = iss_data
|
||||
self._state = None
|
||||
self._attributes = {}
|
||||
self._client_name = name
|
||||
self._name = ATTR_ISS_VISIBLE
|
||||
self._unit_of_measurement = None
|
||||
self._icon = 'mdi:eye'
|
||||
self._name = name
|
||||
self._show_on_map = show
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{} {}'.format(self._client_name, self._name)
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
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
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
if self.iss_data:
|
||||
attrs = {
|
||||
ATTR_ISS_NUMBER_PEOPLE_SPACE:
|
||||
self.iss_data.number_of_people_in_space,
|
||||
ATTR_ISS_NEXT_RISE: self.iss_data.next_rise,
|
||||
}
|
||||
if self._show_on_map:
|
||||
attrs[ATTR_LONGITUDE] = self.iss_data.position.get('longitude')
|
||||
attrs[ATTR_LATITUDE] = self.iss_data.position.get('latitude')
|
||||
else:
|
||||
attrs['long'] = self.iss_data.position.get('longitude')
|
||||
attrs['lat'] = self.iss_data.position.get('latitude')
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from ISS API and updates the states."""
|
||||
self._state = self.iss_data.is_above
|
||||
|
||||
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)
|
||||
self.iss_data.update()
|
||||
|
||||
|
||||
class IssData(object):
|
||||
"""Get data from the ISS."""
|
||||
"""Get data from the ISS API."""
|
||||
|
||||
def __init__(self, latitude, longitude):
|
||||
"""Initialize the data object."""
|
||||
self.is_above = None
|
||||
self.next_rise = None
|
||||
self.number_of_people_in_space = None
|
||||
self.position = None
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from the ISS."""
|
||||
"""Get the latest data from the ISS API."""
|
||||
import pyiss
|
||||
|
||||
try:
|
||||
@ -121,7 +125,7 @@ class IssData(object):
|
||||
self.is_above = iss.is_ISS_above(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()
|
||||
_LOGGER.error(self.next_rise.tzinfo)
|
||||
self.position = iss.current_location()
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error(error)
|
||||
return False
|
@ -7,14 +7,10 @@ https://home-assistant.io/components/binary_sensor.nest/
|
||||
from itertools import chain
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
|
||||
@ -42,17 +38,6 @@ _BINARY_TYPES_DEPRECATED = [
|
||||
|
||||
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_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__)
|
||||
|
||||
@ -63,15 +48,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
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:
|
||||
wstr = (variable + " is no a longer supported "
|
||||
"monitored_conditions. See "
|
||||
"https://home-assistant.io/components/binary_sensor.nest/ "
|
||||
"for valid options, or remove monitored_conditions "
|
||||
"entirely to get a reasonable default")
|
||||
"for valid options.")
|
||||
_LOGGER.error(wstr)
|
||||
|
||||
sensors = []
|
||||
@ -80,16 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
nest.cameras())
|
||||
for structure, device in device_chain:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
for variable in conditions
|
||||
if variable in BINARY_TYPES]
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
for variable in conditions
|
||||
if variable in CLIMATE_BINARY_TYPES
|
||||
and device.is_thermostat]
|
||||
|
||||
if device.is_camera:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
for variable in conditions
|
||||
if variable in CAMERA_BINARY_TYPES]
|
||||
for activity_zone in device.activity_zones:
|
||||
sensors += [NestActivityZoneSensor(structure,
|
||||
|
@ -1,19 +1,19 @@
|
||||
"""
|
||||
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
|
||||
https://home-assistant.io/components/binary_sensor.netatmo/
|
||||
https://home-assistant.io/components/binary_sensor.netatmo/.
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
from homeassistant.components.netatmo import CameraData
|
||||
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
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
@ -22,24 +22,37 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
"Someone known": 'occupancy',
|
||||
"Someone unknown": 'motion',
|
||||
"Motion": 'motion',
|
||||
WELCOME_SENSOR_TYPES = {
|
||||
"Someone known": "motion",
|
||||
"Someone unknown": "motion",
|
||||
"Motion": "motion",
|
||||
"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_CAMERAS = 'cameras'
|
||||
CONF_WELCOME_SENSORS = 'welcome_sensors'
|
||||
CONF_PRESENCE_SENSORS = 'presence_sensors'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_OFFSET): cv.positive_int,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
vol.Optional(
|
||||
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')
|
||||
home = config.get(CONF_HOME, None)
|
||||
timeout = config.get(CONF_TIMEOUT, 15)
|
||||
offset = config.get(CONF_OFFSET, 90)
|
||||
|
||||
module_name = None
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
data = CameraData(netatmo.NETATMO_AUTH, home)
|
||||
if data.get_camera_names() == []:
|
||||
return None
|
||||
except lnetatmo.NoDevice:
|
||||
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():
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in sensors:
|
||||
if variable in ('Tag Vibration', 'Tag Open'):
|
||||
continue
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, module_name,
|
||||
home, timeout, variable)])
|
||||
camera_type = data.get_camera_type(camera=camera_name, home=home)
|
||||
if camera_type == "NACamera":
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in welcome_sensors:
|
||||
add_devices([NetatmoBinarySensor(data, camera_name,
|
||||
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 variable in sensors:
|
||||
for variable in welcome_sensors:
|
||||
if variable in ('Tag Vibration', 'Tag Open'):
|
||||
add_devices([WelcomeBinarySensor(data, camera_name,
|
||||
add_devices([NetatmoBinarySensor(data, camera_name,
|
||||
module_name, home,
|
||||
timeout, variable)])
|
||||
timeout, offset,
|
||||
camera_type,
|
||||
variable)])
|
||||
|
||||
|
||||
class WelcomeBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||
class NetatmoBinarySensor(BinarySensorDevice):
|
||||
"""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."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._module_name = module_name
|
||||
self._home = home
|
||||
self._timeout = timeout
|
||||
self._offset = offset
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
@ -99,10 +132,11 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
||||
self._name += ' / ' + module_name
|
||||
self._sensor_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']
|
||||
self._unique_id = "Welcome_binary_sensor {0} - {1}".format(self._name,
|
||||
self._unique_id = "Netatmo_binary_sensor {0} - {1}".format(self._name,
|
||||
camera_id)
|
||||
self._cameratype = camera_type
|
||||
self.update()
|
||||
|
||||
@property
|
||||
@ -118,7 +152,12 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""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
|
||||
def is_on(self):
|
||||
@ -130,30 +169,54 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
||||
self._data.update()
|
||||
self._data.update_event()
|
||||
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneKnownSeen(self._home,
|
||||
if self._cameratype == "NACamera":
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.camera_data.someoneKnownSeen(self._home,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneUnknownSeen(self._home,
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
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._timeout*60)
|
||||
elif self._sensor_name == "Motion":
|
||||
self._state =\
|
||||
self._data.welcomedata.motionDetected(self._home,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
else:
|
||||
return None
|
||||
elif self._cameratype == "NOC":
|
||||
if self._sensor_name == "Outdoor motion":
|
||||
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":
|
||||
self._state =\
|
||||
self._data.welcomedata.moduleMotionDetected(self._home,
|
||||
self._data.camera_data.moduleMotionDetected(self._home,
|
||||
self._module_name,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Tag Open":
|
||||
self._state =\
|
||||
self._data.welcomedata.moduleOpened(self._home,
|
||||
self._data.camera_data.moduleOpened(self._home,
|
||||
self._module_name,
|
||||
self._camera_name)
|
||||
else:
|
||||
|
@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for port_num, port_name in ports.items():
|
||||
binary_sensors.append(RPiGPIOBinarySensor(
|
||||
port_name, port_num, pull_mode, bouncetime, invert_logic))
|
||||
add_devices(binary_sensors)
|
||||
add_devices(binary_sensors, True)
|
||||
|
||||
|
||||
class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
@ -65,9 +65,9 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
self._pull_mode = pull_mode
|
||||
self._bouncetime = bouncetime
|
||||
self._invert_logic = invert_logic
|
||||
self._state = None
|
||||
|
||||
rpi_gpio.setup_input(self._port, self._pull_mode)
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
|
||||
def read_gpio(port):
|
||||
"""Read state from GPIO."""
|
||||
@ -90,3 +90,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
def is_on(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state != self._invert_logic
|
||||
|
||||
def update(self):
|
||||
"""Update the GPIO state."""
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
|
@ -1,7 +1,23 @@
|
||||
# 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:
|
||||
description: Send a restart command to a ffmpeg based sensor (party mode).
|
||||
description: Send a restart command to a ffmpeg based sensor.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
|
@ -110,7 +110,7 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
return self._sensor_class
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
|
@ -8,7 +8,6 @@ at https://home-assistant.io/components/binary_sensor.wink/
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
@ -43,6 +42,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for hub in pywink.get_hubs():
|
||||
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):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
@ -50,33 +58,13 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
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()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.capability == "loudness":
|
||||
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
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
@ -91,6 +79,11 @@ class WinkHub(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""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."""
|
||||
@ -99,7 +92,59 @@ class WinkHub(WinkDevice, BinarySensorDevice, Entity):
|
||||
'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
|
||||
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 {
|
||||
'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()
|
||||
|
@ -8,7 +8,6 @@ import logging
|
||||
import datetime
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
@ -65,21 +64,14 @@ def setup_platform(hass, config, add_devices, discovery_info=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."""
|
||||
|
||||
def __init__(self, value, sensor_class):
|
||||
"""Initialize the sensor."""
|
||||
self._sensor_type = sensor_class
|
||||
# pylint: disable=import-error
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
@ -95,32 +87,25 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""No polling needed."""
|
||||
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, Entity):
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
"""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."""
|
||||
super(ZWaveTriggerSensor, self).__init__(sensor_value, sensor_class)
|
||||
super(ZWaveTriggerSensor, self).__init__(value, sensor_class)
|
||||
self._hass = hass
|
||||
self.re_arm_sec = re_arm_sec
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
# If it's active make sure that we set the timeout tracker
|
||||
if sensor_value.data:
|
||||
if value.data:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
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:
|
||||
self.schedule_update_ha_state()
|
||||
if value.data:
|
||||
|
@ -6,14 +6,17 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import asyncio
|
||||
import collections
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
from random import SystemRandom
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
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.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -35,6 +39,9 @@ STATE_IDLE = 'idle'
|
||||
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
||||
_RND = SystemRandom()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -89,13 +105,8 @@ class Camera(Entity):
|
||||
def __init__(self):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
self._access_token = hashlib.sha256(
|
||||
str.encode(str(id(self)))).hexdigest()
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
"""Access token for this camera."""
|
||||
return self._access_token
|
||||
self.access_tokens = collections.deque([], 2)
|
||||
self.async_update_token()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -105,7 +116,7 @@ class Camera(Entity):
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""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
|
||||
def is_recording(self):
|
||||
@ -174,7 +185,7 @@ class Camera(Entity):
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
except (asyncio.CancelledError, ConnectionResetError):
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
@ -196,7 +207,7 @@ class Camera(Entity):
|
||||
def state_attributes(self):
|
||||
"""Camera state attributes."""
|
||||
attr = {
|
||||
'access_token': self.access_token,
|
||||
'access_token': self.access_tokens[-1],
|
||||
}
|
||||
|
||||
if self.model:
|
||||
@ -207,6 +218,13 @@ class Camera(Entity):
|
||||
|
||||
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):
|
||||
"""Base CameraView."""
|
||||
@ -223,10 +241,11 @@ class CameraView(HomeAssistantView):
|
||||
camera = self.entities.get(entity_id)
|
||||
|
||||
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
|
||||
request.GET.get('token') == camera.access_token)
|
||||
request.GET.get('token') in camera.access_tokens)
|
||||
|
||||
if not authenticated:
|
||||
return web.Response(status=401)
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/camera.amcrest/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.loader as loader
|
||||
@ -13,16 +15,20 @@ from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
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__)
|
||||
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'mjpeg'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
@ -32,6 +38,14 @@ RESOLUTION_LIST = {
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1
|
||||
}
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
TIMEOUT = 5
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
@ -40,19 +54,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
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):
|
||||
"""Set up an Amcrest IP Camera."""
|
||||
from amcrest import AmcrestCamera
|
||||
data = AmcrestCamera(
|
||||
camera = AmcrestCamera(
|
||||
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')
|
||||
try:
|
||||
data.camera.current_time
|
||||
camera.current_time
|
||||
# pylint: disable=broad-except
|
||||
except Exception as 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)
|
||||
return False
|
||||
|
||||
add_devices([AmcrestCam(config, data)])
|
||||
add_devices([AmcrestCam(hass, config, camera)])
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestCam(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."""
|
||||
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._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):
|
||||
"""Return a still image reponse from the camera."""
|
||||
# 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
|
||||
|
||||
@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
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
|
@ -12,10 +12,9 @@ from aiohttp import web
|
||||
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
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
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""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
|
||||
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||
|
||||
@ -44,20 +43,17 @@ class FFmpegCamera(Camera):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a FFmpeg camera."""
|
||||
super().__init__()
|
||||
|
||||
self._manager = hass.data[DATA_FFMPEG]
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._input = config.get(CONF_INPUT)
|
||||
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
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageSingleAsync, IMAGE_JPEG
|
||||
ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop)
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
|
||||
image = yield from ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
@ -67,9 +63,9 @@ class FFmpegCamera(Camera):
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""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(
|
||||
self._input, extra_cmd=self._extra_arguments)
|
||||
|
||||
|
@ -9,8 +9,6 @@ import logging
|
||||
from contextlib import closing
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
@ -20,18 +18,21 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_MJPEG_URL = 'mjpeg_url'
|
||||
CONF_STILL_IMAGE_URL = 'still_image_url'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
DEFAULT_NAME = 'Mjpeg Camera'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_MJPEG_URL): cv.url,
|
||||
vol.Optional(CONF_STILL_IMAGE_URL): cv.url,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@ -70,6 +71,7 @@ class MjpegCamera(Camera):
|
||||
self._username = device_info.get(CONF_USERNAME)
|
||||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._mjpeg_url = device_info[CONF_MJPEG_URL]
|
||||
self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL)
|
||||
|
||||
self._auth = None
|
||||
if self._username and self._password:
|
||||
@ -78,6 +80,37 @@ class MjpegCamera(Camera):
|
||||
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):
|
||||
"""Return a still image response from the camera."""
|
||||
if self._username and self._password:
|
||||
@ -103,36 +136,9 @@ class MjpegCamera(Camera):
|
||||
|
||||
# connect to stream
|
||||
websession = async_get_clientsession(self.hass)
|
||||
stream = None
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
stream = yield from websession.get(self._mjpeg_url,
|
||||
auth=self._auth)
|
||||
stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
|
||||
|
||||
response = web.StreamResponse()
|
||||
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()
|
||||
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/camera.netatmo/
|
||||
https://home-assistant.io/components/camera.netatmo/.
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
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.loader import get_component
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@ -30,41 +30,43 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
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')
|
||||
home = config.get(CONF_HOME)
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
data = CameraData(netatmo.NETATMO_AUTH, home)
|
||||
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 config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
add_devices([NetatmoCamera(data, camera_name, home, camera_type)])
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
|
||||
class WelcomeCamera(Camera):
|
||||
"""Representation of the images published from Welcome camera."""
|
||||
class NetatmoCamera(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."""
|
||||
super(WelcomeCamera, self).__init__()
|
||||
super(NetatmoCamera, self).__init__()
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||
camera_id = data.camera_data.cameraByName(camera=camera_name,
|
||||
home=home)['id']
|
||||
self._unique_id = "Welcome_camera {0} - {1}".format(self._name,
|
||||
camera_id)
|
||||
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
|
||||
self._vpnurl, self._localurl = self._data.camera_data.cameraUrls(
|
||||
camera=camera_name
|
||||
)
|
||||
self._cameratype = camera_type
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
@ -79,15 +81,30 @@ class WelcomeCamera(Camera):
|
||||
_LOGGER.error('Welcome VPN url changed: %s', error)
|
||||
self._data.update()
|
||||
(self._vpnurl, self._localurl) = \
|
||||
self._data.welcomedata.cameraUrls(camera=self._camera_name)
|
||||
self._data.camera_data.cameraUrls(camera=self._camera_name)
|
||||
return None
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this Netatmo Welcome device."""
|
||||
"""Return the name of this Netatmo camera device."""
|
||||
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
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
|
@ -10,8 +10,6 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
@ -20,7 +18,8 @@ from homeassistant.const import (
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
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
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
@ -253,38 +252,10 @@ class SynologyCamera(Camera):
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
stream = None
|
||||
response = None
|
||||
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)
|
||||
stream_coro = self._websession.get(
|
||||
streaming_url, params=streaming_payload)
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
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()
|
||||
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -32,6 +32,7 @@ SERVICE_SET_AWAY_MODE = "set_away_mode"
|
||||
SERVICE_SET_AUX_HEAT = "set_aux_heat"
|
||||
SERVICE_SET_TEMPERATURE = "set_temperature"
|
||||
SERVICE_SET_FAN_MODE = "set_fan_mode"
|
||||
SERVICE_SET_HOLD_MODE = "set_hold_mode"
|
||||
SERVICE_SET_OPERATION_MODE = "set_operation_mode"
|
||||
SERVICE_SET_SWING_MODE = "set_swing_mode"
|
||||
SERVICE_SET_HUMIDITY = "set_humidity"
|
||||
@ -56,6 +57,7 @@ ATTR_CURRENT_HUMIDITY = "current_humidity"
|
||||
ATTR_HUMIDITY = "humidity"
|
||||
ATTR_MAX_HUMIDITY = "max_humidity"
|
||||
ATTR_MIN_HUMIDITY = "min_humidity"
|
||||
ATTR_HOLD_MODE = "hold_mode"
|
||||
ATTR_OPERATION_MODE = "operation_mode"
|
||||
ATTR_OPERATION_LIST = "operation_list"
|
||||
ATTR_SWING_MODE = "swing_mode"
|
||||
@ -93,6 +95,10 @@ SET_FAN_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
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({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_OPERATION_MODE): cv.string,
|
||||
@ -116,9 +122,23 @@ def set_away_mode(hass, away_mode, entity_id=None):
|
||||
if 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)
|
||||
|
||||
|
||||
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):
|
||||
"""Turn all or specified climate devices auxillary heater on."""
|
||||
data = {
|
||||
@ -229,6 +249,8 @@ def async_setup(hass, config):
|
||||
SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE)
|
||||
return
|
||||
|
||||
_LOGGER.warning(
|
||||
'This service has been deprecated; use climate.set_hold_mode')
|
||||
for climate in target_climate:
|
||||
if away_mode:
|
||||
yield from climate.async_turn_away_mode_on()
|
||||
@ -242,6 +264,23 @@ def async_setup(hass, config):
|
||||
descriptions.get(SERVICE_SET_AWAY_MODE),
|
||||
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
|
||||
def async_aux_heat_set_service(service):
|
||||
"""Set auxillary heater on target climate devices."""
|
||||
@ -446,6 +485,10 @@ class ClimateDevice(Entity):
|
||||
if 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
|
||||
if swing_mode is not None:
|
||||
data[ATTR_SWING_MODE] = swing_mode
|
||||
@ -517,6 +560,11 @@ class ClimateDevice(Entity):
|
||||
"""Return true if away mode is on."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return the current hold mode, e.g., home, away, temp."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if aux heater."""
|
||||
@ -626,6 +674,18 @@ class ClimateDevice(Entity):
|
||||
return self.hass.loop.run_in_executor(
|
||||
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):
|
||||
"""Turn auxillary heater on."""
|
||||
raise NotImplementedError()
|
||||
|
@ -12,11 +12,11 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo climate devices."""
|
||||
add_devices([
|
||||
DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low",
|
||||
None, None, "Auto", "heat", None, None, None),
|
||||
DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High",
|
||||
DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, None, 77,
|
||||
"Auto Low", None, None, "Auto", "heat", None, None, None),
|
||||
DemoClimate("Hvac", 21, TEMP_CELSIUS, True, None, 22, "On High",
|
||||
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)
|
||||
])
|
||||
|
||||
@ -25,7 +25,7 @@ class DemoClimate(ClimateDevice):
|
||||
"""Representation of a demo climate device."""
|
||||
|
||||
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,
|
||||
current_operation, aux, target_temp_high, target_temp_low):
|
||||
"""Initialize the climate device."""
|
||||
@ -34,6 +34,7 @@ class DemoClimate(ClimateDevice):
|
||||
self._target_humidity = target_humidity
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._away = away
|
||||
self._hold = hold
|
||||
self._current_temperature = current_temperature
|
||||
self._current_humidity = current_humidity
|
||||
self._current_fan_mode = current_fan_mode
|
||||
@ -106,6 +107,11 @@ class DemoClimate(ClimateDevice):
|
||||
"""Return if away mode is on."""
|
||||
return self._away
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return hold mode setting."""
|
||||
return self._hold
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
@ -171,6 +177,11 @@ class DemoClimate(ClimateDevice):
|
||||
self._away = False
|
||||
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):
|
||||
"""Turn away auxillary heater on."""
|
||||
self._aux = True
|
||||
|
@ -11,10 +11,10 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ecobee
|
||||
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)
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@ -145,12 +145,30 @@ class Thermostat(ClimateDevice):
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""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
|
||||
def target_temperature_high(self):
|
||||
"""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
|
||||
def desired_fan_mode(self):
|
||||
@ -165,6 +183,19 @@ class Thermostat(ClimateDevice):
|
||||
else:
|
||||
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
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
@ -218,54 +249,110 @@ class Thermostat(ClimateDevice):
|
||||
"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
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
mode = self.mode
|
||||
events = self.thermostat['events']
|
||||
for event in events:
|
||||
if event['holdClimateRef'] == 'away' or \
|
||||
event['type'] == 'autoAway':
|
||||
mode = "away"
|
||||
break
|
||||
return 'away' in mode
|
||||
return any(event['holdClimateRef'] == 'away' or
|
||||
event['type'] == 'autoAway'
|
||||
for event in events)
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
if self.hold_temp:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", "indefinite")
|
||||
else:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index, "away")
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", self.hold_preference())
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""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
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \
|
||||
kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None:
|
||||
high_temp = int(kwargs.get(ATTR_TARGET_TEMP_LOW))
|
||||
low_temp = int(kwargs.get(ATTR_TARGET_TEMP_HIGH))
|
||||
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if self.hold_temp:
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
|
||||
high_temp, "indefinite")
|
||||
_LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, "
|
||||
"high=%s, is=%s", low_temp, isinstance(
|
||||
low_temp, (int, float)), high_temp,
|
||||
isinstance(high_temp, (int, float)))
|
||||
if self.current_operation == STATE_AUTO and low_temp is not None \
|
||||
and high_temp is not None:
|
||||
self.set_auto_temp_hold(int(low_temp), int(high_temp))
|
||||
elif temp is not None:
|
||||
self.set_temp_hold(int(temp))
|
||||
else:
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
|
||||
high_temp)
|
||||
_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
|
||||
_LOGGER.error(
|
||||
'Missing valid arguments for set_temperature in %s', kwargs)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
|
||||
@ -284,15 +371,19 @@ class Thermostat(ClimateDevice):
|
||||
str(resume_all).lower())
|
||||
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):
|
||||
# """ 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)
|
||||
# Sleep mode isn't used in UI yet:
|
||||
|
||||
# def turn_sleep_mode_on(self):
|
||||
# """ Turns sleep mode on. """
|
||||
|
@ -76,6 +76,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
self._name = _name
|
||||
self._thermostat = eq3.Thermostat(_mac)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if thermostat is available."""
|
||||
return self.current_operation != STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
|
@ -87,6 +87,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self._unit = hass.config.units.temperature_unit
|
||||
|
||||
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)
|
||||
if sensor_state:
|
||||
@ -134,7 +135,7 @@ class GenericThermostat(ClimateDevice):
|
||||
return
|
||||
self._target_temp = temperature
|
||||
self._control_heating()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
@ -165,6 +166,12 @@ class GenericThermostat(ClimateDevice):
|
||||
self._control_heating()
|
||||
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):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['evohomeclient==0.2.5',
|
||||
'somecomfort==0.3.2']
|
||||
'somecomfort==0.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,6 +22,18 @@ set_away_mode:
|
||||
description: New value of away mode
|
||||
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:
|
||||
description: Set target temperature of climate device
|
||||
|
||||
|
@ -52,8 +52,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
|
||||
def __init__(self, value, temp_unit):
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
@ -71,9 +69,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
self.update_properties()
|
||||
# register listener
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
@ -85,16 +80,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
" workaround")
|
||||
self._zxt_120 = 1
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self.update_properties()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
"""Callback on data changes for node values."""
|
||||
# Operation Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
|
@ -53,8 +53,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
import libopenzwave
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._lozwmgr = libopenzwave.PyManager()
|
||||
@ -62,8 +60,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
self._node = value.node
|
||||
self._current_position = None
|
||||
self._workaround = None
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
@ -74,16 +70,8 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
_LOGGER.debug("Controller without positioning feedback")
|
||||
self._workaround = 1
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self.update_properties()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
"""Callback on data changes for node values."""
|
||||
# Position value
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
@ -160,24 +148,12 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave garage door."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._state = value.data
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
self._state = value.data
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of Zwave garage door."""
|
||||
return not self._state
|
||||
return not self._value.data
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the garage door."""
|
||||
|
@ -71,10 +71,11 @@ _ARP_REGEX = re.compile(
|
||||
|
||||
_IP_NEIGH_CMD = 'ip neigh'
|
||||
_IP_NEIGH_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
|
||||
r'\w+\s' +
|
||||
r'\w+\s' +
|
||||
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3}|'
|
||||
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'(?P<status>(\w+))')
|
||||
|
||||
_NVRAM_CMD = 'nvram get client_info_tmp'
|
||||
@ -323,6 +324,8 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
|
||||
else:
|
||||
for lease in result.leases:
|
||||
if lease.startswith(b'duid '):
|
||||
continue
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
if not match:
|
||||
|
@ -15,9 +15,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['https://github.com/deisi/fritzconnection/archive/'
|
||||
'b5c14515e1c8e2652b06b6316a7f3913df942841.zip'
|
||||
'#fritzconnection==0.4.6']
|
||||
REQUIREMENTS = ['fritzconnection==0.6']
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
107
homeassistant/components/device_tracker/linksys_ap.py
Normal file
107
homeassistant/components/device_tracker/linksys_ap.py
Normal 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})
|
126
homeassistant/components/device_tracker/sky_hub.py
Normal file
126
homeassistant/components/device_tracker/sky_hub.py
Normal 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
|
129
homeassistant/components/device_tracker/tado.py
Normal file
129
homeassistant/components/device_tracker/tado.py
Normal 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
|
@ -12,6 +12,7 @@ import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
@ -29,6 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
CMD_LOGIN = 15
|
||||
CMD_LOGOUT = 16
|
||||
CMD_DEVICES = 123
|
||||
|
||||
|
||||
@ -62,7 +64,21 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
}
|
||||
|
||||
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
|
||||
def async_scan_devices(self):
|
||||
@ -74,12 +90,14 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
return []
|
||||
|
||||
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)
|
||||
return [mac.text for mac in xml_root.iter('MACAddr')]
|
||||
try:
|
||||
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
|
||||
def async_get_device_name(self, device):
|
||||
@ -92,6 +110,7 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
response = None
|
||||
try:
|
||||
# get first token
|
||||
self.websession.cookie_jar.clear()
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.get(
|
||||
"http://{}/common_page/login.html".format(self.host)
|
||||
@ -107,7 +126,7 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
})
|
||||
|
||||
# successfull?
|
||||
if data.find("successful") != -1:
|
||||
if data is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -31,12 +31,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""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
|
||||
|
||||
|
||||
class XioamiDeviceScanner(DeviceScanner):
|
||||
class XiaomiDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Xiaomi Mi router.
|
||||
|
||||
Adapted from Luci scanner.
|
||||
@ -44,15 +44,14 @@ class XioamiDeviceScanner(DeviceScanner):
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
self.token = _get_token(host, username, password)
|
||||
|
||||
self.host = host
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
|
||||
self.mac2name = 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."""
|
||||
with self.lock:
|
||||
if self.mac2name is None:
|
||||
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
|
||||
url = url.format(self.host, self.token)
|
||||
result = _get_device_list(url)
|
||||
result = self._retrieve_list_with_retry()
|
||||
if result:
|
||||
hosts = [x for x in result
|
||||
if 'mac' in x and 'name' in x]
|
||||
@ -76,7 +73,7 @@ class XioamiDeviceScanner(DeviceScanner):
|
||||
(x['mac'].upper(), x['name']) for x in hosts]
|
||||
self.mac2name = dict(mac2name_list)
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
# Error, handled in the _retrieve_list_with_retry
|
||||
return
|
||||
return self.mac2name.get(device.upper(), None)
|
||||
|
||||
@ -90,29 +87,72 @@ class XioamiDeviceScanner(DeviceScanner):
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info('Refreshing device list')
|
||||
url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
|
||||
url = url.format(self.host, self.token)
|
||||
result = _get_device_list(url)
|
||||
result = self._retrieve_list_with_retry()
|
||||
if result:
|
||||
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'])
|
||||
|
||||
self._store_result(result)
|
||||
return True
|
||||
|
||||
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:
|
||||
res = requests.get(url, timeout=5, **kwargs)
|
||||
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 _extract_result(res, 'list')
|
||||
|
||||
|
||||
def _get_token(host, username, password):
|
||||
@ -124,10 +164,6 @@ def _get_token(host, username, password):
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.exception('Connection to the router timed out')
|
||||
return
|
||||
return _extract_result(res, 'token')
|
||||
|
||||
|
||||
def _extract_result(res, key_name):
|
||||
if res.status_code == 200:
|
||||
try:
|
||||
result = res.json()
|
||||
@ -136,10 +172,12 @@ def _extract_result(res, key_name):
|
||||
_LOGGER.exception('Failed to parse response from mi router')
|
||||
return
|
||||
try:
|
||||
return result[key_name]
|
||||
return result['token']
|
||||
except KeyError:
|
||||
_LOGGER.exception('No %s in response from mi router. %s',
|
||||
key_name, result)
|
||||
error_message = "Xiaomi token cannot be refreshed, response from "\
|
||||
+ "url: [%s] \nwith parameter: [%s] \nwas: [%s]"
|
||||
_LOGGER.exception(error_message, url, data, result)
|
||||
return
|
||||
else:
|
||||
_LOGGER.error('Invalid response from mi router: %s', res)
|
||||
_LOGGER.error('Invalid response: [%s] at url: [%s] with data [%s]',
|
||||
res, url, data)
|
||||
|
@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/emulated_hue/
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@ -13,6 +14,7 @@ from homeassistant import util
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.components.http import REQUIREMENTS # NOQA
|
||||
from homeassistant.components.http import HomeAssistantWSGI
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from .hue_api import (
|
||||
@ -24,8 +26,13 @@ DOMAIN = 'emulated_hue'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NUMBERS_FILE = 'emulated_hue_ids.json'
|
||||
|
||||
CONF_HOST_IP = 'host_ip'
|
||||
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_EXPOSE_BY_DEFAULT = 'expose_by_default'
|
||||
CONF_EXPOSED_DOMAINS = 'exposed_domains'
|
||||
@ -35,18 +42,23 @@ TYPE_ALEXA = 'alexa'
|
||||
TYPE_GOOGLE = 'google_home'
|
||||
|
||||
DEFAULT_LISTEN_PORT = 8300
|
||||
DEFAULT_UPNP_BIND_MULTICAST = True
|
||||
DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
|
||||
DEFAULT_EXPOSE_BY_DEFAULT = True
|
||||
DEFAULT_EXPOSED_DOMAINS = [
|
||||
'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan'
|
||||
]
|
||||
DEFAULT_TYPE = TYPE_ALEXA
|
||||
DEFAULT_TYPE = TYPE_GOOGLE
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_HOST_IP): cv.string,
|
||||
vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_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_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
|
||||
@ -60,7 +72,7 @@ ATTR_EMULATED_HUE = 'emulated_hue'
|
||||
|
||||
def setup(hass, yaml_config):
|
||||
"""Activate the emulated_hue component."""
|
||||
config = Config(yaml_config.get(DOMAIN, {}))
|
||||
config = Config(hass, yaml_config.get(DOMAIN, {}))
|
||||
|
||||
server = HomeAssistantWSGI(
|
||||
hass,
|
||||
@ -84,7 +96,9 @@ def setup(hass, yaml_config):
|
||||
server.register_view(HueOneLightChangeView(config))
|
||||
|
||||
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
|
||||
def stop_emulated_hue_bridge(event):
|
||||
@ -108,12 +122,17 @@ def setup(hass, yaml_config):
|
||||
class Config(object):
|
||||
"""Holds configuration variables for the emulated hue bridge."""
|
||||
|
||||
def __init__(self, conf):
|
||||
def __init__(self, hass, conf):
|
||||
"""Initialize the instance."""
|
||||
self.hass = hass
|
||||
self.type = conf.get(CONF_TYPE)
|
||||
self.numbers = {}
|
||||
self.numbers = None
|
||||
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
|
||||
self.host_ip_addr = conf.get(CONF_HOST_IP)
|
||||
if self.host_ip_addr is None:
|
||||
@ -134,6 +153,11 @@ class Config(object):
|
||||
_LOGGER.warning('When targetting Google Home, listening port has '
|
||||
'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"
|
||||
# This is primarily useful for things like scenes or scripts, which
|
||||
# don't really have a concept of being off
|
||||
@ -151,11 +175,21 @@ class Config(object):
|
||||
self.exposed_domains = conf.get(
|
||||
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):
|
||||
"""Get a unique number for the entity id."""
|
||||
if self.type == TYPE_ALEXA:
|
||||
return entity_id
|
||||
|
||||
if self.numbers is None:
|
||||
self.numbers = self._load_numbers_json()
|
||||
|
||||
# Google Home
|
||||
for number, ent_id in self.numbers.items():
|
||||
if entity_id == ent_id:
|
||||
@ -163,6 +197,7 @@ class Config(object):
|
||||
|
||||
number = str(len(self.numbers) + 1)
|
||||
self.numbers[number] = entity_id
|
||||
self._save_numbers_json()
|
||||
return number
|
||||
|
||||
def number_to_entity_id(self, number):
|
||||
@ -170,6 +205,9 @@ class Config(object):
|
||||
if self.type == TYPE_ALEXA:
|
||||
return number
|
||||
|
||||
if self.numbers is None:
|
||||
self.numbers = self._load_numbers_json()
|
||||
|
||||
# Google Home
|
||||
assert isinstance(number, str)
|
||||
return self.numbers.get(number)
|
||||
@ -196,3 +234,26 @@ class Config(object):
|
||||
domain_exposed_by_default and explicit_expose is not False
|
||||
|
||||
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)
|
||||
|
@ -17,6 +17,10 @@ from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -174,7 +178,9 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
# Make sure the entity actually supports brightness
|
||||
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:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
@ -207,6 +213,23 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
else:
|
||||
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:
|
||||
# Map the off command to on
|
||||
service = SERVICE_TURN_ON
|
||||
@ -269,7 +292,9 @@ def parse_hue_api_put_light_body(request_json, entity):
|
||||
report_brightness = True
|
||||
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
|
||||
level = brightness / 255 * 100
|
||||
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)
|
||||
# Convert 0.0-1.0 to 0-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:
|
||||
final_state, final_brightness = cached_state
|
||||
# Make sure brightness is valid
|
||||
|
@ -50,7 +50,7 @@ class DescriptionXmlView(HomeAssistantView):
|
||||
"""
|
||||
|
||||
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')
|
||||
|
||||
@ -60,12 +60,14 @@ class UPNPResponderThread(threading.Thread):
|
||||
|
||||
_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."""
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.host_ip_addr = host_ip_addr
|
||||
self.listen_port = listen_port
|
||||
self.upnp_bind_multicast = upnp_bind_multicast
|
||||
|
||||
# Note that the double newline at the end of
|
||||
# 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) \
|
||||
.replace("\n", "\r\n") \
|
||||
.encode('utf-8')
|
||||
self.upnp_response = resp_template.format(
|
||||
advertise_ip, advertise_port).replace("\n", "\r\n") \
|
||||
.encode('utf-8')
|
||||
|
||||
# Set up a pipe for signaling to the receiver that it's time to
|
||||
# shutdown. Essentially, we place the SSDP socket into nonblocking
|
||||
@ -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(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:
|
||||
if self._interrupted:
|
||||
|
@ -41,7 +41,6 @@ SERVICE_SET_DIRECTION = 'set_direction'
|
||||
|
||||
SPEED_OFF = 'off'
|
||||
SPEED_LOW = 'low'
|
||||
SPEED_MED = 'med'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
||||
@ -230,6 +229,9 @@ class FanEntity(ToggleEntity):
|
||||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
if speed is SPEED_OFF:
|
||||
self.turn_off()
|
||||
return
|
||||
raise NotImplementedError()
|
||||
|
||||
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:
|
||||
"""Turn on the fan."""
|
||||
if speed is SPEED_OFF:
|
||||
self.turn_off()
|
||||
return
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
|
@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH,
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
FanEntity, SUPPORT_SET_SPEED,
|
||||
SUPPORT_OSCILLATE, SUPPORT_DIRECTION)
|
||||
from homeassistant.const import STATE_OFF
|
||||
@ -54,9 +54,9 @@ class DemoFan(FanEntity):
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""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."""
|
||||
self.set_speed(speed)
|
||||
|
||||
|
@ -8,7 +8,7 @@ import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF,
|
||||
SPEED_LOW, SPEED_MED,
|
||||
SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH)
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_UNKNOWN, STATE_ON, STATE_OFF
|
||||
@ -20,8 +20,8 @@ VALUE_TO_STATE = {
|
||||
0: SPEED_OFF,
|
||||
63: SPEED_LOW,
|
||||
64: SPEED_LOW,
|
||||
190: SPEED_MED,
|
||||
191: SPEED_MED,
|
||||
190: SPEED_MEDIUM,
|
||||
191: SPEED_MEDIUM,
|
||||
255: SPEED_HIGH,
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ STATE_TO_VALUE = {}
|
||||
for key in VALUE_TO_STATE:
|
||||
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
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||
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,
|
||||
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
|
||||
SPEED_OFF, ATTR_SPEED)
|
||||
@ -64,11 +64,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_OSCILLATION_OFF,
|
||||
default=DEFAULT_PAYLOAD_OFF): 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_SPEED_LIST,
|
||||
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,
|
||||
})
|
||||
|
||||
@ -162,7 +162,7 @@ class MqttFan(FanEntity):
|
||||
if payload == self._payload[SPEED_LOW]:
|
||||
self._speed = SPEED_LOW
|
||||
elif payload == self._payload[SPEED_MEDIUM]:
|
||||
self._speed = SPEED_MED
|
||||
self._speed = SPEED_MEDIUM
|
||||
elif payload == self._payload[SPEED_HIGH]:
|
||||
self._speed = SPEED_HIGH
|
||||
self.update_ha_state()
|
||||
@ -235,11 +235,12 @@ class MqttFan(FanEntity):
|
||||
"""Return the oscillation state."""
|
||||
return self._oscillation
|
||||
|
||||
def turn_on(self, speed: str=SPEED_MED) -> None:
|
||||
def turn_on(self, speed: str=None) -> None:
|
||||
"""Turn on the entity."""
|
||||
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC],
|
||||
self._payload[STATE_ON], self._qos, self._retain)
|
||||
self.set_speed(speed)
|
||||
if speed:
|
||||
self.set_speed(speed)
|
||||
|
||||
def turn_off(self) -> None:
|
||||
"""Turn off the entity."""
|
||||
@ -252,7 +253,7 @@ class MqttFan(FanEntity):
|
||||
mqtt_payload = SPEED_OFF
|
||||
if speed == SPEED_LOW:
|
||||
mqtt_payload = self._payload[SPEED_LOW]
|
||||
elif speed == SPEED_MED:
|
||||
elif speed == SPEED_MEDIUM:
|
||||
mqtt_payload = self._payload[SPEED_MEDIUM]
|
||||
elif speed == SPEED_HIGH:
|
||||
mqtt_payload = self._payload[SPEED_HIGH]
|
||||
@ -265,9 +266,12 @@ class MqttFan(FanEntity):
|
||||
|
||||
def oscillate(self, oscillating: bool) -> None:
|
||||
"""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
|
||||
payload = self._payload[OSCILLATE_ON_PAYLOAD]
|
||||
if oscillating is False:
|
||||
payload = self._payload[OSCILLATE_OFF_PAYLOAD]
|
||||
mqtt.publish(self._hass,
|
||||
self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
|
||||
self._oscillation, self._qos, self._retain)
|
||||
payload, self._qos, self._retain)
|
||||
self.update_ha_state()
|
||||
|
@ -50,4 +50,15 @@ toggle:
|
||||
fields:
|
||||
entity_id:
|
||||
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'
|
||||
|
@ -32,7 +32,7 @@ class WinkFanDevice(WinkDevice, FanEntity):
|
||||
"""Initialize the fan."""
|
||||
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."""
|
||||
self.wink.set_fan_direction(direction)
|
||||
|
||||
|
@ -10,13 +10,14 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DOMAIN = 'ffmpeg'
|
||||
REQUIREMENTS = ["ha-ffmpeg==0.15"]
|
||||
REQUIREMENTS = ["ha-ffmpeg==1.2"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_FFMPEG = 'ffmpeg'
|
||||
|
||||
CONF_INPUT = 'input'
|
||||
CONF_FFMPEG_BIN = 'ffmpeg_bin'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
@ -34,53 +35,54 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, 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
|
||||
def async_run_test(hass, input_source):
|
||||
"""Run test on this input. TRUE is deactivate or run correct.
|
||||
def async_setup(hass, config):
|
||||
"""Setup the FFmpeg component."""
|
||||
conf = config.get(DOMAIN, {})
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
from haffmpeg import TestAsync
|
||||
hass.data[DATA_FFMPEG] = FFmpegManager(
|
||||
hass,
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
@ -1,18 +1,18 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "22d39af274e1d824ca1302e10971f2d8",
|
||||
"frontend.html": "61e57194179b27563a05282b58dd4f47",
|
||||
"mdi.html": "5bb2f1717206bad0d187c2633062c575",
|
||||
"core.js": "769f3fdd4e04b34bd66c7415743cf7b5",
|
||||
"frontend.html": "d48d9a13f7d677e59b1d22c6db051207",
|
||||
"mdi.html": "7a0f14bbf3822449f9060b9c53bd7376",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5",
|
||||
"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-template.html": "cbb251acabd5e7431058ed507b70522b",
|
||||
"panels/ha-panel-history.html": "7baeb4bd7d9ce0def4f95eab6f10812e",
|
||||
"panels/ha-panel-history.html": "9f2c72574fb6135beb1b381a4b8b7703",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "93de4cee3a2352a6813b5c218421d534",
|
||||
"panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89",
|
||||
"panels/ha-panel-logbook.html": "313f2ac57aaa5ad55933c9bbf8d8a1e5",
|
||||
"panels/ha-panel-map.html": "13f120066c0b5faa2ce1db2c3f3cc486",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit 988ac0028163cfc970e781718bc9459ed486ea61
|
||||
Subproject commit 5159326a7b3d1ba29ae17a7861fa2eaa8c2c95f6
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -75,8 +75,15 @@ Polymer({
|
||||
},
|
||||
|
||||
fitMap: function () {
|
||||
var bounds = new window.L.latLngBounds(
|
||||
this._mapItems.map(function (item) { return item.getLatLng(); }));
|
||||
var bounds;
|
||||
|
||||
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));
|
||||
},
|
||||
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1,27 +1,113 @@
|
||||
"""
|
||||
CEC component.
|
||||
HDMI CEC component.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/hdmi_cec/
|
||||
"""
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from functools import reduce
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START, CONF_DEVICES)
|
||||
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
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE = 'device'
|
||||
REQUIREMENTS = ['pyCEC==0.4.12']
|
||||
|
||||
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_SELECT_DEVICE = 'select_device'
|
||||
SERVICE_STANDBY = 'standby'
|
||||
|
||||
# pylint: disable=unnecessary-lambda
|
||||
@ -30,92 +116,312 @@ DEVICE_SCHEMA = vol.Schema({
|
||||
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({
|
||||
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)
|
||||
|
||||
|
||||
def pad_physical_address(addr):
|
||||
"""Right-pad a physical address."""
|
||||
return addr + [0] * (4 - len(addr))
|
||||
|
||||
|
||||
def parse_mapping(mapping, parents=None):
|
||||
"""Parse configuration device mapping."""
|
||||
if parents is None:
|
||||
parents = []
|
||||
for addr, val in mapping.items():
|
||||
cur = parents + [str(addr)]
|
||||
if isinstance(val, dict):
|
||||
yield from parse_mapping(val, cur)
|
||||
elif isinstance(val, str):
|
||||
yield (val, cur)
|
||||
if isinstance(addr, (str,)) and isinstance(val, (str,)):
|
||||
from pycec.network import PhysicalAddress
|
||||
yield (addr, PhysicalAddress(val))
|
||||
else:
|
||||
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):
|
||||
"""Right-pad a physical address."""
|
||||
return addr + ['0'] * (MAX_DEPTH - len(addr))
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
def setup(hass: HomeAssistant, base_config):
|
||||
"""Setup CEC capability."""
|
||||
global _CEC
|
||||
|
||||
try:
|
||||
import cec
|
||||
except ImportError:
|
||||
_LOGGER.error("libcec must be installed")
|
||||
return False
|
||||
from pycec.network import HDMINetwork
|
||||
from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand
|
||||
from pycec.const import KEY_VOLUME_UP, KEY_VOLUME_DOWN, KEY_MUTE_ON, \
|
||||
KEY_MUTE_OFF, KEY_MUTE_TOGGLE, ADDR_AUDIOSYSTEM, ADDR_BROADCAST, \
|
||||
ADDR_UNREGISTERED
|
||||
from pycec.cec import CecAdapter
|
||||
from pycec.tcp import TcpAdapter
|
||||
|
||||
# Parse configuration into a dict of device name to physical address
|
||||
# represented as a list of four elements.
|
||||
flat = {}
|
||||
for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})):
|
||||
flat[pair[0]] = pad_physical_address(pair[1])
|
||||
device_aliases = {}
|
||||
devices = base_config[DOMAIN].get(CONF_DEVICES, {})
|
||||
_LOGGER.debug("Parsing config %s", devices)
|
||||
device_aliases.update(parse_mapping(devices))
|
||||
_LOGGER.debug("Parsed devices: %s", device_aliases)
|
||||
|
||||
# Configure libcec.
|
||||
cfg = cec.libcec_configuration()
|
||||
cfg.strDeviceName = 'HASS'
|
||||
cfg.bActivateSource = 0
|
||||
cfg.bMonitorOnly = 1
|
||||
cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT
|
||||
platform = base_config[DOMAIN].get(CONF_PLATFORM, SWITCH)
|
||||
|
||||
# Setup CEC adapter.
|
||||
_CEC = cec.ICECAdapter.Create(cfg)
|
||||
loop = (
|
||||
# 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):
|
||||
"""Power on all devices."""
|
||||
_CEC.PowerOnDevices()
|
||||
def _volume(call):
|
||||
"""Increase/decrease volume and mute/unmute system."""
|
||||
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):
|
||||
"""Standby all devices."""
|
||||
_CEC.StandbyDevices()
|
||||
hdmi_network.standby()
|
||||
|
||||
@callback
|
||||
def _power_on(call):
|
||||
hdmi_network.power_on()
|
||||
|
||||
def _select_device(call):
|
||||
"""Select the active device."""
|
||||
path = flat.get(call.data[ATTR_DEVICE])
|
||||
if not path:
|
||||
from pycec.network import PhysicalAddress
|
||||
|
||||
addr = call.data[ATTR_DEVICE]
|
||||
if not addr:
|
||||
_LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE])
|
||||
cmds = []
|
||||
for i in range(1, MAX_DEPTH - 1):
|
||||
addr = pad_physical_address(path[:i])
|
||||
cmds.append('1f:82:{}{}:{}{}'.format(*addr))
|
||||
cmds.append('1f:86:{}{}:{}{}'.format(*addr))
|
||||
for cmd in cmds:
|
||||
_CEC.Transmit(_CEC.CommandFromString(cmd))
|
||||
_LOGGER.info("Selected %s", call.data[ATTR_DEVICE])
|
||||
return
|
||||
if addr in device_aliases:
|
||||
addr = device_aliases[addr]
|
||||
else:
|
||||
entity = hass.states.get(addr)
|
||||
_LOGGER.debug("Selecting entity %s", entity)
|
||||
if entity is not None:
|
||||
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):
|
||||
"""Open CEC adapter."""
|
||||
adapters = _CEC.DetectAdapters()
|
||||
if len(adapters) == 0:
|
||||
_LOGGER.error("No CEC adapter found")
|
||||
return
|
||||
"""Register services and start HDMI network to watch for devices."""
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))[DOMAIN]
|
||||
hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, _tx,
|
||||
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):
|
||||
hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on)
|
||||
hass.services.register(DOMAIN, SERVICE_STANDBY, _standby)
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE,
|
||||
_select_device)
|
||||
else:
|
||||
_LOGGER.error("Failed to open adapter")
|
||||
hdmi_network.set_new_device_callback(_new_device)
|
||||
hdmi_network.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
||||
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
|
||||
|
@ -23,7 +23,7 @@ from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ["pyhomematic==0.1.19"]
|
||||
REQUIREMENTS = ["pyhomematic==0.1.20"]
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@ -68,7 +68,7 @@ HM_DEVICE_TYPES = {
|
||||
DISCOVER_BINARY_SENSORS: [
|
||||
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
|
||||
'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact',
|
||||
'HMWIOSwitch'],
|
||||
'HMWIOSwitch', 'MaxShutterContact'],
|
||||
DISCOVER_COVER: ['Blind', 'KeyBlind']
|
||||
}
|
||||
|
||||
@ -432,8 +432,8 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
}, config)
|
||||
|
||||
|
||||
def _get_devices(hass, device_type, keys, proxy):
|
||||
"""Get the Homematic devices."""
|
||||
def _get_devices(hass, discovery_type, keys, proxy):
|
||||
"""Get the Homematic devices for given discovery_type."""
|
||||
device_arr = []
|
||||
|
||||
for key in keys:
|
||||
@ -441,14 +441,14 @@ def _get_devices(hass, device_type, keys, proxy):
|
||||
class_name = device.__class__.__name__
|
||||
metadata = {}
|
||||
|
||||
# is class supported by discovery type
|
||||
if class_name not in HM_DEVICE_TYPES[device_type]:
|
||||
# Class supported by discovery type
|
||||
if class_name not in HM_DEVICE_TYPES[discovery_type]:
|
||||
continue
|
||||
|
||||
# Load metadata if needed to generate a param list
|
||||
if device_type == DISCOVER_SENSORS:
|
||||
if discovery_type == DISCOVER_SENSORS:
|
||||
metadata.update(device.SENSORNODE)
|
||||
elif device_type == DISCOVER_BINARY_SENSORS:
|
||||
elif discovery_type == DISCOVER_BINARY_SENSORS:
|
||||
metadata.update(device.BINARYNODE)
|
||||
else:
|
||||
metadata.update({None: device.ELEMENT})
|
||||
@ -459,8 +459,9 @@ def _get_devices(hass, device_type, keys, proxy):
|
||||
if param in HM_IGNORE_DISCOVERY_NODE:
|
||||
continue
|
||||
|
||||
# add devices
|
||||
_LOGGER.debug("Handling %s: %s", param, channels)
|
||||
# Add devices
|
||||
_LOGGER.debug("%s: Handling %s: %s: %s",
|
||||
discovery_type, key, param, channels)
|
||||
for channel in channels:
|
||||
name = _create_ha_name(
|
||||
name=device.NAME, channel=channel, param=param,
|
||||
@ -485,7 +486,7 @@ def _get_devices(hass, device_type, keys, proxy):
|
||||
str(err))
|
||||
else:
|
||||
_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
|
||||
|
||||
|
||||
@ -873,7 +874,7 @@ class HMDevice(Entity):
|
||||
(self._hmdevice.SENSORNODE, self._hmdevice.getSensorData),
|
||||
(self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)):
|
||||
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)
|
||||
|
||||
return True
|
||||
|
@ -36,6 +36,7 @@ CONF_SOURCE = 'source'
|
||||
CONF_CONFIDENCE = 'confidence'
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
DEFAULT_CONFIDENCE = 80
|
||||
|
||||
SOURCE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
@ -44,6 +45,8 @@ SOURCE_SCHEMA = vol.Schema({
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
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({
|
||||
@ -95,6 +98,11 @@ class ImageProcessingEntity(Entity):
|
||||
"""Return camera entity id from process pictures."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def confidence(self):
|
||||
"""Return minimum confidence for do some things."""
|
||||
return None
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process image."""
|
||||
raise NotImplementedError()
|
||||
|
@ -8,13 +8,17 @@ https://home-assistant.io/components/demo/
|
||||
from homeassistant.components.image_processing import ImageProcessingEntity
|
||||
from homeassistant.components.image_processing.openalpr_local import (
|
||||
ImageProcessingAlprEntity)
|
||||
from homeassistant.components.image_processing.microsoft_face_identify import (
|
||||
ImageProcessingFaceIdentifyEntity)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the demo image_processing platform."""
|
||||
add_devices([
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
@ -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)
|
@ -26,25 +26,26 @@ _LOGGER = logging.getLogger(__name__)
|
||||
OPENALPR_API_URL = "https://api.openalpr.com/v1/recognize"
|
||||
|
||||
OPENALPR_REGIONS = [
|
||||
'us',
|
||||
'eu',
|
||||
'au',
|
||||
'auwide',
|
||||
'br',
|
||||
'eu',
|
||||
'fr',
|
||||
'gb',
|
||||
'kr',
|
||||
'kr2',
|
||||
'mx',
|
||||
'sg',
|
||||
'us',
|
||||
'vn2'
|
||||
]
|
||||
|
||||
CONF_REGION = 'region'
|
||||
DEFAULT_CONFIDENCE = 80
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_REGION):
|
||||
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))
|
||||
})
|
||||
|
||||
|
||||
|
@ -31,28 +31,30 @@ ATTR_PLATES = 'plates'
|
||||
ATTR_VEHICLES = 'vehicles'
|
||||
|
||||
OPENALPR_REGIONS = [
|
||||
'us',
|
||||
'eu',
|
||||
'au',
|
||||
'auwide',
|
||||
'br',
|
||||
'eu',
|
||||
'fr',
|
||||
'gb',
|
||||
'kr',
|
||||
'kr2',
|
||||
'mx',
|
||||
'sg',
|
||||
'us',
|
||||
'vn2'
|
||||
]
|
||||
|
||||
|
||||
CONF_REGION = 'region'
|
||||
CONF_ALPR_BIN = 'alp_bin'
|
||||
|
||||
DEFAULT_BINARY = 'alpr'
|
||||
DEFAULT_CONFIDENCE = 80
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_REGION):
|
||||
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
|
||||
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.vehicles = 0 # vehicles count
|
||||
|
||||
@property
|
||||
def confidence(self):
|
||||
"""Return minimum confidence for send events."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
|
@ -244,9 +244,7 @@ def setup(hass, config):
|
||||
if CONFIG_FILE == {}:
|
||||
CONFIG_FILE[ATTR_DEVICES] = {}
|
||||
|
||||
# Notify needs to have discovery
|
||||
# notify_config = {"notify": {CONF_PLATFORM: "ios"}}
|
||||
# bootstrap.setup_component(hass, "notify", notify_config)
|
||||
discovery.load_platform(hass, "notify", DOMAIN, {}, config)
|
||||
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
|
||||
|
@ -228,10 +228,6 @@ class ISYDevice(Entity):
|
||||
self._change_handler = self._node.status.subscribe('changed',
|
||||
self.on_update)
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""Cleanup the subscriptions."""
|
||||
self._change_handler.unsubscribe()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Handle the update event from the ISY994 Node."""
|
||||
@ -272,7 +268,7 @@ class ISYDevice(Entity):
|
||||
return self._node.status._val
|
||||
|
||||
@property
|
||||
def state_attributes(self) -> Dict:
|
||||
def device_state_attributes(self) -> Dict:
|
||||
"""Get the state attributes for the device."""
|
||||
attr = {}
|
||||
if hasattr(self._node, 'aux_properties'):
|
||||
|
@ -1,32 +1,8 @@
|
||||
"""
|
||||
Receive signals from a keyboard and use it as a remote control.
|
||||
|
||||
This component allows to use a keyboard as remote control. It will
|
||||
fire ´keyboard_remote_command_received´ events witch can then be used
|
||||
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
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/keyboard_remote/
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error
|
||||
@ -48,14 +24,20 @@ REQUIREMENTS = ['evdev==0.6.1']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ICON = 'mdi:remote'
|
||||
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_VALUE = {'key_up': 0, 'key_down': 1, 'key_hold': 2}
|
||||
TYPE = 'type'
|
||||
DEVICE_DESCRIPTOR = 'device_descriptor'
|
||||
DEVICE_NAME = 'device_name'
|
||||
DEVICE_ID_GROUP = 'Device descriptor or name'
|
||||
|
||||
|
||||
CONFIG_SCHEMA = 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.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold')),
|
||||
}),
|
||||
@ -65,22 +47,15 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
def setup(hass, config):
|
||||
"""Setup keyboard_remote."""
|
||||
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(
|
||||
hass,
|
||||
device_descriptor,
|
||||
key_value
|
||||
config
|
||||
)
|
||||
|
||||
def _start_keyboard_remote(_event):
|
||||
@ -104,60 +79,93 @@ def setup(hass, config):
|
||||
class KeyboardRemote(threading.Thread):
|
||||
"""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."""
|
||||
from evdev import InputDevice
|
||||
from evdev import InputDevice, list_devices
|
||||
|
||||
self.device_descriptor = device_descriptor
|
||||
try:
|
||||
self.dev = InputDevice(device_descriptor)
|
||||
except OSError: # Keyboard not present
|
||||
_LOGGER.debug(
|
||||
'KeyboardRemote: keyboard not connected, %s',
|
||||
self.device_descriptor)
|
||||
self.keyboard_connected = False
|
||||
self.device_descriptor = config.get(DEVICE_DESCRIPTOR)
|
||||
self.device_name = config.get(DEVICE_NAME)
|
||||
if self.device_descriptor:
|
||||
self.device_id = self.device_descriptor
|
||||
else:
|
||||
self.keyboard_connected = True
|
||||
self.device_id = self.device_name
|
||||
self.dev = self._get_keyboard_device()
|
||||
if self.dev is not None:
|
||||
_LOGGER.debug(
|
||||
'KeyboardRemote: keyboard connected, %s',
|
||||
self.dev)
|
||||
'Keyboard connected, %s',
|
||||
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)
|
||||
self.stopped = threading.Event()
|
||||
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):
|
||||
"""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()
|
||||
_LOGGER.debug(
|
||||
'KeyboardRemote interface started for %s',
|
||||
'Interface started for %s',
|
||||
self.dev)
|
||||
|
||||
while not self.stopped.isSet():
|
||||
# Sleeps to ease load on processor
|
||||
time.sleep(.1)
|
||||
|
||||
if not self.keyboard_connected:
|
||||
try:
|
||||
self.dev = InputDevice(self.device_descriptor)
|
||||
except OSError: # still disconnected
|
||||
continue
|
||||
else:
|
||||
if self.dev is None:
|
||||
self.dev = self._get_keyboard_device()
|
||||
if self.dev is not None:
|
||||
self.dev.grab()
|
||||
self.keyboard_connected = True
|
||||
_LOGGER.debug('KeyboardRemote: keyboard re-connected, %s',
|
||||
self.device_descriptor)
|
||||
self.hass.bus.fire(
|
||||
KEYBOARD_REMOTE_CONNECTED
|
||||
)
|
||||
_LOGGER.debug('Keyboard re-connected, %s',
|
||||
self.device_id)
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
event = self.dev.read_one()
|
||||
except IOError: # Keyboard Disconnected
|
||||
self.keyboard_connected = False
|
||||
_LOGGER.debug('KeyboardRemote: keyard disconnected, %s',
|
||||
self.device_descriptor)
|
||||
self.dev = None
|
||||
self.hass.bus.fire(
|
||||
KEYBOARD_REMOTE_DISCONNECTED
|
||||
)
|
||||
_LOGGER.debug('Keyboard disconnected, %s',
|
||||
self.device_id)
|
||||
continue
|
||||
|
||||
if not event:
|
||||
|
118
homeassistant/components/light/avion.py
Normal file
118
homeassistant/components/light/avion.py
Normal 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
|
124
homeassistant/components/light/decora.py
Normal file
124
homeassistant/components/light/decora.py
Normal 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()
|
@ -25,6 +25,7 @@ from homeassistant.components.light import (
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
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_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({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean,
|
||||
vol.Optional(CONF_FILENAME): cv.string,
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_ALLOW_UNREACHABLE,
|
||||
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"
|
||||
@ -63,6 +75,8 @@ SCENE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_SCENE_NAME): cv.string,
|
||||
})
|
||||
|
||||
ATTR_IS_HUE_GROUP = "is_hue_group"
|
||||
|
||||
|
||||
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
|
||||
"""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):
|
||||
"""Setup the Hue lights."""
|
||||
# Default needed in case of discovery
|
||||
filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE)
|
||||
allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE,
|
||||
DEFAULT_ALLOW_UNREACHABLE)
|
||||
filename = config.get(CONF_FILENAME)
|
||||
allow_unreachable = config.get(CONF_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:
|
||||
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:
|
||||
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."""
|
||||
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)
|
||||
|
||||
request_configuration(host, hass, add_devices, filename,
|
||||
allow_unreachable)
|
||||
allow_unreachable, allow_in_emulated_hue,
|
||||
allow_hue_groups)
|
||||
|
||||
return
|
||||
|
||||
@ -143,7 +161,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
|
||||
|
||||
lights = {}
|
||||
lightgroups = {}
|
||||
skip_groups = False
|
||||
skip_groups = not allow_hue_groups
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_lights():
|
||||
@ -184,7 +202,8 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights,
|
||||
bridge_type, allow_unreachable)
|
||||
bridge_type, allow_unreachable,
|
||||
allow_in_emulated_hue)
|
||||
new_lights.append(lights[light_id])
|
||||
else:
|
||||
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:
|
||||
lightgroups[lightgroup_id] = HueLight(
|
||||
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])
|
||||
else:
|
||||
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,
|
||||
allow_unreachable):
|
||||
allow_unreachable, allow_in_emulated_hue,
|
||||
allow_hue_groups):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
|
||||
@ -243,7 +264,8 @@ def request_configuration(host, hass, add_devices, filename,
|
||||
# pylint: disable=unused-argument
|
||||
def hue_configuration_callback(data):
|
||||
"""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(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
@ -259,7 +281,8 @@ class HueLight(Light):
|
||||
"""Representation of a Hue light."""
|
||||
|
||||
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."""
|
||||
self.light_id = light_id
|
||||
self.info = info
|
||||
@ -268,6 +291,7 @@ class HueLight(Light):
|
||||
self.bridge_type = bridge_type
|
||||
self.allow_unreachable = allow_unreachable
|
||||
self.is_group = is_group
|
||||
self.allow_in_emulated_hue = allow_in_emulated_hue
|
||||
|
||||
if is_group:
|
||||
self._command_func = self.bridge.set_group
|
||||
@ -395,3 +419,13 @@ class HueLight(Light):
|
||||
def update(self):
|
||||
"""Synchronize state with bridge."""
|
||||
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
|
||||
|
@ -38,15 +38,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
setup_light(device_id, conf_lights[device_id], insteonhub, hass,
|
||||
add_devices)
|
||||
|
||||
linked = insteonhub.get_linked()
|
||||
else:
|
||||
linked = insteonhub.get_linked()
|
||||
|
||||
for device_id in linked:
|
||||
if (linked[device_id]['cat_type'] == 'dimmer' and
|
||||
device_id not in conf_lights):
|
||||
request_configuration(device_id,
|
||||
insteonhub,
|
||||
linked[device_id]['model_name'] + ' ' +
|
||||
linked[device_id]['sku'], hass, add_devices)
|
||||
for device_id in linked:
|
||||
if (linked[device_id]['cat_type'] == 'dimmer' and
|
||||
device_id not in conf_lights):
|
||||
request_configuration(device_id,
|
||||
insteonhub,
|
||||
linked[device_id]['model_name'] + ' ' +
|
||||
linked[device_id]['sku'],
|
||||
hass, add_devices)
|
||||
|
||||
|
||||
def request_configuration(device_id, insteonhub, model, hass,
|
||||
|
@ -8,18 +8,13 @@ import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.light import (
|
||||
Light, SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS)
|
||||
Light, SUPPORT_BRIGHTNESS)
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
False: STATE_OFF,
|
||||
True: STATE_ON,
|
||||
}
|
||||
|
||||
UOM = ['2', '51', '78']
|
||||
STATES = [STATE_OFF, STATE_ON, 'true', 'false', '%']
|
||||
|
||||
@ -52,12 +47,12 @@ class ISYLightDevice(isy.ISYDevice, Light):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 light is on."""
|
||||
return self.state == STATE_ON
|
||||
return self.value > 0
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 light."""
|
||||
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
|
||||
def brightness(self) -> float:
|
||||
"""Get the brightness of the ISY994 light."""
|
||||
return self.value
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""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):
|
||||
_LOGGER.debug('Unable to turn on light.')
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Flag supported attributes."""
|
||||
return {ATTR_BRIGHTNESS: self.value}
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
|
98
homeassistant/components/light/lutron.py
Normal file
98
homeassistant/components/light/lutron.py
Normal 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)
|
104
homeassistant/components/light/piglow.py
Normal file
104
homeassistant/components/light/piglow.py
Normal 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
|
@ -5,8 +5,11 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.qwikswitch/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.qwikswitch as qwikswitch
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['qwikswitch']
|
||||
|
||||
|
||||
@ -14,7 +17,7 @@ DEPENDENCIES = ['qwikswitch']
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Add lights from the main Qwikswitch component."""
|
||||
if discovery_info is None:
|
||||
logging.getLogger(__name__).error('Configure Qwikswitch Component.')
|
||||
_LOGGER.error("Configure Qwikswitch component")
|
||||
return False
|
||||
|
||||
add_devices(qwikswitch.QSUSB['light'])
|
||||
|
@ -83,7 +83,9 @@ class TellstickLight(TellstickDevice, Light):
|
||||
def _send_device_command(self, requested_state, requested_data):
|
||||
"""Let tellcore update the actual device to the requested state."""
|
||||
if requested_state:
|
||||
brightness = requested_data
|
||||
self._tellcore_device.dim(brightness)
|
||||
if requested_data is not None:
|
||||
self._brightness = int(requested_data)
|
||||
|
||||
self._tellcore_device.dim(self._brightness)
|
||||
else:
|
||||
self._tellcore_device.turn_off()
|
||||
|
@ -23,7 +23,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink lights."""
|
||||
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):
|
||||
|
@ -33,16 +33,10 @@ def x10_command(command):
|
||||
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):
|
||||
"""Get on/off status for given unit."""
|
||||
unit = int(code[1:])
|
||||
return get_status()[16 - int(unit)] == '1'
|
||||
output = check_output('heyu onstate ' + code, shell=True)
|
||||
return int(output.decode('utf-8')[0])
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@ -63,8 +57,8 @@ class X10Light(Light):
|
||||
"""Initialize an X10 Light."""
|
||||
self._name = light['name']
|
||||
self._id = light['id']
|
||||
self._is_on = False
|
||||
self._brightness = 0
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -79,7 +73,7 @@ class X10Light(Light):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self._is_on
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@ -90,13 +84,13 @@ class X10Light(Light):
|
||||
"""Instruct the light to turn on."""
|
||||
x10_command('on ' + self._id)
|
||||
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
|
||||
self._is_on = True
|
||||
self._state = True
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Instruct the light to turn off."""
|
||||
x10_command('off ' + self._id)
|
||||
self._is_on = False
|
||||
self._state = False
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for this light."""
|
||||
self._is_on = get_unit_status(self._id)
|
||||
"""Fetch update state."""
|
||||
self._state = bool(get_unit_status(self._id))
|
||||
|
@ -17,6 +17,7 @@ from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
|
||||
color_temperature_mired_to_kelvin, color_temperature_to_rgb, \
|
||||
color_rgb_to_rgbw, color_rgbw_to_rgb
|
||||
from homeassistant.helpers import customize
|
||||
|
||||
_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_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):
|
||||
@ -51,13 +55,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
customize = hass.data['zwave_customize']
|
||||
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)
|
||||
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
|
||||
_LOGGER.debug('customize=%s name=%s node_config=%s CONF_REFRESH_VALUE=%s'
|
||||
' CONF_REFRESH_DELAY=%s', customize, name, node_config,
|
||||
_LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s'
|
||||
' CONF_REFRESH_DELAY=%s', name, node_config,
|
||||
refresh, delay)
|
||||
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||
return
|
||||
@ -87,9 +90,6 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
|
||||
def __init__(self, value, refresh, delay):
|
||||
"""Initialize the light."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._brightness = None
|
||||
self._state = None
|
||||
@ -115,38 +115,33 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
self._timer = None
|
||||
_LOGGER.debug('self._refreshing=%s self.delay=%s',
|
||||
self._refresh_value, self._delay)
|
||||
dispatcher.connect(
|
||||
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def update_properties(self):
|
||||
"""Update internal properties based on zwave values."""
|
||||
# Brightness
|
||||
self._brightness, self._state = brightness_state(self._value)
|
||||
|
||||
def _value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
_LOGGER.debug('Value changed for label %s', self._value.label)
|
||||
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:
|
||||
def value_changed(self, value):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
if self._refresh_value:
|
||||
if self._refreshing:
|
||||
self._refreshing = False
|
||||
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
|
||||
def brightness(self):
|
||||
@ -161,7 +156,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_ZWAVE
|
||||
return SUPPORT_ZWAVE_DIMMER
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
@ -351,3 +346,11 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
|
||||
|
||||
super().turn_on(**kwargs)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
if self._zw098:
|
||||
return SUPPORT_ZWAVE_COLORTEMP
|
||||
else:
|
||||
return SUPPORT_ZWAVE_COLOR
|
||||
|
@ -1,21 +1,57 @@
|
||||
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
|
||||
|
||||
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
|
||||
clear_usercode:
|
||||
description: Clear a usercode from lock
|
||||
|
||||
fields:
|
||||
node_id:
|
||||
description: Node id of the lock
|
||||
example: 18
|
||||
code_slot:
|
||||
description: Code slot to clear code from
|
||||
example: 1
|
||||
|
||||
get_usercode:
|
||||
description: Retrieve a usercode from lock
|
||||
|
||||
fields:
|
||||
node_id:
|
||||
description: Node id of the lock
|
||||
example: 18
|
||||
code_slot:
|
||||
description: Code slot to retrive a code from
|
||||
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
|
||||
|
@ -7,13 +7,24 @@ https://home-assistant.io/components/lock.zwave/
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lock import DOMAIN, LockDevice
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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 = {
|
||||
1: 'Manual Lock',
|
||||
@ -22,18 +33,80 @@ LOCK_NOTIFICATION = {
|
||||
4: 'RF Unlock',
|
||||
5: 'Keypad Lock',
|
||||
6: 'Keypad Unlock',
|
||||
11: 'Lock Jammed',
|
||||
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 = {
|
||||
1: True,
|
||||
2: False,
|
||||
3: True,
|
||||
4: False,
|
||||
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
|
||||
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]]
|
||||
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:
|
||||
return
|
||||
if value.type != zwave.const.TYPE_BOOL:
|
||||
return
|
||||
if value.genre != zwave.const.GENRE_USER:
|
||||
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)
|
||||
add_devices([ZwaveLock(value)])
|
||||
|
||||
@ -60,28 +201,16 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the Z-Wave switch device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
|
||||
self._node = value.node
|
||||
self._state = None
|
||||
self._notification = None
|
||||
dispatcher.connect(
|
||||
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
self._lock_status = None
|
||||
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):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
"""Callback on data changes for node values."""
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_ALARM).values():
|
||||
if value.label != "Access Control":
|
||||
@ -89,9 +218,55 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||
self._notification = LOCK_NOTIFICATION.get(value.data)
|
||||
if self._notification:
|
||||
self._state = LOCK_STATUS.get(value.data)
|
||||
_LOGGER.debug('Lock state set from Access Control value and'
|
||||
' is %s', value.data)
|
||||
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
|
||||
def is_locked(self):
|
||||
@ -112,4 +287,6 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||
data = super().device_state_attributes
|
||||
if self._notification:
|
||||
data[ATTR_NOTIFICATION] = self._notification
|
||||
if self._lock_status:
|
||||
data[ATTR_LOCK_STATUS] = self._lock_status
|
||||
return data
|
||||
|
@ -307,6 +307,10 @@ def _exclude_events(events, config):
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
to_state = State.from_dict(event.data.get('new_state'))
|
||||
# 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:
|
||||
continue
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user