Merge pull request #5530 from home-assistant/dev

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

View File

@ -46,6 +46,9 @@ omit =
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/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
View File

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

View File

@ -8,15 +8,16 @@ matrix:
env: TOXENV=requirements
- 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
View File

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

80
CODE_OF_CONDUCT.md Normal file
View File

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

View File

@ -7,7 +7,7 @@ RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# 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
View File

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

194
LICENSE.md Normal file
View File

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

View File

@ -87,7 +87,7 @@ components <https://home-assistant.io/developers/creating_components/>`__.
If you run into issues while using Home Assistant or during development
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

View File

@ -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

View File

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

View File

@ -63,7 +63,7 @@ def read_input(pin):
"""Read a value from a GPIO."""
# 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):

View File

@ -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)

View File

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

View File

@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""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,

View File

@ -4,8 +4,9 @@ Provides a binary sensor which is a collection of ffmpeg tools.
For more details about this platform, please refer to the documentation at
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

View File

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

View File

@ -5,34 +5,39 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.iss/
"""
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

View File

@ -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,

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -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,

View File

@ -8,7 +8,6 @@ at https://home-assistant.io/components/binary_sensor.wink/
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.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()

View File

@ -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:

View File

@ -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)

View File

@ -4,8 +4,10 @@ This component provides basic support for Amcrest IP cameras.
For more details about this platform, please refer to the documentation at
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."""

View File

@ -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)

View File

@ -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):

View File

@ -1,15 +1,15 @@
"""
Support for the Netatmo Welcome camera.
Support for the Netatmo cameras.
For more details about this platform, please refer to the documentation at
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."""

View File

@ -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):

View File

@ -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()

View File

@ -12,11 +12,11 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""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

View File

@ -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. """

View File

@ -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."""

View File

@ -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)

View File

@ -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__)

View File

@ -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

View File

@ -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():

View File

@ -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."""

View File

@ -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:

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import aiohttp
import async_timeout
import 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

View File

@ -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)

View File

@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/
"""
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)

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -75,8 +75,15 @@ Polymer({
},
fitMap: function () {
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));
},

File diff suppressed because one or more lines are too long

View File

@ -1,27 +1,113 @@
"""
CEC component.
HDMI CEC component.
For more details about this component, please refer to the documentation at
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

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

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

View File

@ -26,25 +26,26 @@ _LOGGER = logging.getLogger(__name__)
OPENALPR_API_URL = "https://api.openalpr.com/v1/recognize"
OPENALPR_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))
})

View File

@ -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."""

View File

@ -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)

View File

@ -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'):

View File

@ -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:

View File

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

View File

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

View File

@ -25,6 +25,7 @@ from homeassistant.components.light import (
from homeassistant.config import load_yaml_config_file
from homeassistant.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

View File

@ -38,15 +38,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
setup_light(device_id, conf_lights[device_id], insteonhub, hass,
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,

View File

@ -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."""

View File

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

View File

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

View File

@ -5,8 +5,11 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/
"""
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'])

View File

@ -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()

View File

@ -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):

View File

@ -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))

View File

@ -17,6 +17,7 @@ from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
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

View File

@ -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

View File

@ -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

View File

@ -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