Merge remote-tracking branch 'upstream/dev' into igd

This commit is contained in:
Steven Looman 2018-09-30 20:22:10 +02:00
commit f511920a04
770 changed files with 20553 additions and 7864 deletions

View File

@ -42,6 +42,7 @@ omit =
homeassistant/components/asterisk_mbox.py
homeassistant/components/*/asterisk_mbox.py
homeassistant/components/*/asterisk_cdr.py
homeassistant/components/august.py
homeassistant/components/*/august.py
@ -92,6 +93,9 @@ omit =
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
homeassistant/components/edp_redy.py
homeassistant/components/*/edp_redy.py
homeassistant/components/egardia.py
homeassistant/components/*/egardia.py
@ -101,6 +105,9 @@ omit =
homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py
homeassistant/components/evohome.py
homeassistant/components/*/evohome.py
homeassistant/components/fritzbox.py
homeassistant/components/switch/fritzbox.py
@ -123,7 +130,8 @@ omit =
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
homeassistant/components/*/hangouts.py
homeassistant/components/hangouts/intents.py
homeassistant/components/*/hangouts.py
homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py
@ -140,17 +148,20 @@ omit =
homeassistant/components/homematicip_cloud.py
homeassistant/components/*/homematicip_cloud.py
homeassistant/components/huawei_lte.py
homeassistant/components/*/huawei_lte.py
homeassistant/components/hydrawise.py
homeassistant/components/*/hydrawise.py
homeassistant/components/ihc/*
homeassistant/components/*/ihc.py
homeassistant/components/insteon/*
homeassistant/components/*/insteon.py
homeassistant/components/insteon_local.py
homeassistant/components/insteon_plm.py
homeassistant/components/ios.py
@ -183,6 +194,9 @@ omit =
homeassistant/components/linode.py
homeassistant/components/*/linode.py
homeassistant/components/logi_circle.py
homeassistant/components/*/logi_circle.py
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
@ -228,7 +242,7 @@ omit =
homeassistant/components/opencv.py
homeassistant/components/*/opencv.py
homeassistant/components/openuv.py
homeassistant/components/openuv/__init__.py
homeassistant/components/*/openuv.py
homeassistant/components/pilight.py
@ -377,6 +391,7 @@ omit =
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/alarm_control_panel/totalconnect.py
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
homeassistant/components/apiai.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/concord232.py
@ -414,6 +429,7 @@ omit =
homeassistant/components/climate/honeywell.py
homeassistant/components/climate/knx.py
homeassistant/components/climate/oem.py
homeassistant/components/climate/opentherm_gw.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/climate/sensibo.py
@ -497,6 +513,7 @@ omit =
homeassistant/components/light/lw12wifi.py
homeassistant/components/light/mystrom.py
homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/opple.py
homeassistant/components/light/osramlightify.py
homeassistant/components/light/piglow.py
homeassistant/components/light/rpi_gpio_pwm.py
@ -666,6 +683,7 @@ omit =
homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/gearbest.py
homeassistant/components/sensor/geizhals.py
homeassistant/components/sensor/gitlab_ci.py
homeassistant/components/sensor/gitter.py
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py
@ -683,6 +701,7 @@ omit =
homeassistant/components/sensor/kwb.py
homeassistant/components/sensor/lacrosse.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/linky.py
homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/luftdaten.py
@ -739,6 +758,7 @@ omit =
homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/spotcrime.py
homeassistant/components/sensor/starlingbank.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
@ -793,6 +813,7 @@ omit =
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/snmp.py
homeassistant/components/switch/switchbot.py
homeassistant/components/switch/switchmate.py
homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py
@ -810,6 +831,7 @@ omit =
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
homeassistant/components/weather/met.py
homeassistant/components/weather/metoffice.py
homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/zamg.py

View File

@ -3,7 +3,7 @@
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
## Example entry for `configuration.yaml` (if applicable):
```yaml
@ -15,7 +15,7 @@
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
If the code communicates with devices, web services, or third-party tools:
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).

View File

@ -50,6 +50,7 @@ homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/huawei_router.py @abmantis
homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/lifx.py @amelchio
@ -72,6 +73,7 @@ homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/jewish_calendar.py @tsvi
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pollen.py @bachya
@ -91,11 +93,15 @@ homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610
homeassistant/components/ecovacs.py @OverloadUT
homeassistant/components/*/ecovacs.py @OverloadUT
homeassistant/components/edp_redy.py @abmantis
homeassistant/components/*/edp_redy.py @abmantis
homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p
homeassistant/components/huawei_lte.py @scop
homeassistant/components/*/huawei_lte.py @scop
homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342
homeassistant/components/konnected.py @heythisisnate
@ -116,9 +122,13 @@ homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen
homeassistant/components/upcloud.py @scop
homeassistant/components/*/upcloud.py @scop
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
homeassistant/components/zoneminder.py @rohankapoorcom
homeassistant/components/*/zoneminder.py @rohankapoorcom
homeassistant/scripts/check_config.py @kellerza

View File

@ -10,5 +10,5 @@ The process is straight-forward.
- Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
Still interested? Then you should take a peek at the [developer documentation](https://home-assistant.io/developers/) to get more details.
Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details.

View File

@ -1,194 +1,201 @@
Apache License
==============
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
### Terms and Conditions for use, reproduction, and distribution
1. Definitions.
#### 1. Definitions
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
“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.
“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.
“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.
“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.
“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.
“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).
“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.
“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."
“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.
“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.
#### 2. Grant of Copyright License
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.
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.
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:
#### 3. Grant of Patent License
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
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.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
#### 4. Redistribution
(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
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:
(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.
* **(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.
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.
#### 5. Submission of 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.
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.
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.
#### 6. Trademarks
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.
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.
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.
#### 7. Disclaimer of Warranty
END OF TERMS AND CONDITIONS
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.
APPENDIX: How to apply the Apache License to your work.
#### 8. Limitation of Liability
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.
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.
Copyright [yyyy] [name of copyright owner]
#### 9. Accepting Warranty or Additional Liability
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
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.
http://www.apache.org/licenses/LICENSE-2.0
_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.
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

@ -21,8 +21,8 @@ Featured integrations
|screenshot-components|
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://home-assistant.io/developers/architecture/>`__ and the `section on creating your own
components <https://home-assistant.io/developers/creating_components/>`__.
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://developers.home-assistant.io/docs/en/architecture_index.html>`__ and the `section on creating your own
components <https://developers.home-assistant.io/docs/en/creating_component_index.html>`__.
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/>`__ of our website for further help and information.

View File

@ -19,4 +19,4 @@ Indices and tables
* :ref:`modindex`
* :ref:`search`
.. _Home Assistant developers: https://home-assistant.io/developers/
.. _Home Assistant developers: https://developers.home-assistant.io/

View File

@ -7,7 +7,6 @@ import platform
import subprocess
import sys
import threading
from typing import List, Dict, Any # noqa pylint: disable=unused-import
@ -20,15 +19,19 @@ from homeassistant.const import (
)
def attempt_use_uvloop() -> None:
def set_loop() -> None:
"""Attempt to use uvloop."""
import asyncio
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
if sys.platform == 'win32':
asyncio.set_event_loop(asyncio.ProactorEventLoop())
else:
try:
import uvloop
except ImportError:
pass
else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
def validate_python() -> None:
@ -240,51 +243,39 @@ def cmdline() -> List[str]:
return [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> int:
async def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> int:
"""Set up HASS and run."""
from homeassistant import bootstrap
from homeassistant import bootstrap, core
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
nt_args = cmdline() + ['--runner']
while True:
try:
subprocess.check_call(nt_args)
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
sys.exit(exc.returncode)
hass = core.HomeAssistant()
if args.demo_mode:
config = {
'frontend': {},
'demo': {}
} # type: Dict[str, Any]
hass = bootstrap.from_config_dict(
config, config_dir=config_dir, verbose=args.verbose,
bootstrap.async_from_config_dict(
config, hass, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
log_file=args.log_file, log_no_color=args.log_no_color)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
hass = bootstrap.from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
await bootstrap.async_from_config_file(
config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
log_no_color=args.log_no_color)
if hass is None:
return -1
if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch
from homeassistant.util.async_ import run_callback_threadsafe
def open_browser(_: Any) -> None:
"""Open the web interface in a browser."""
if hass.config.api is not None: # type: ignore
if hass.config.api is not None:
import webbrowser
webbrowser.open(hass.config.api.base_url) # type: ignore
webbrowser.open(hass.config.api.base_url)
run_callback_threadsafe(
hass.loop,
@ -292,7 +283,7 @@ def setup_and_run_hass(config_dir: str,
EVENT_HOMEASSISTANT_START, open_browser
)
return hass.start()
return await hass.async_run()
def try_to_restart() -> None:
@ -347,7 +338,20 @@ def main() -> int:
monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks()
attempt_use_uvloop()
set_loop()
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
nt_args = cmdline() + ['--runner']
while True:
try:
subprocess.check_call(nt_args)
sys.exit(0)
except KeyboardInterrupt:
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
sys.exit(exc.returncode)
args = get_arguments()
@ -366,11 +370,12 @@ def main() -> int:
if args.pid_file:
write_pid(args.pid_file)
exit_code = setup_and_run_hass(config_dir, args)
from homeassistant.util.async_ import asyncio_run
exit_code = asyncio_run(setup_and_run_hass(config_dir, args))
if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart()
return exit_code
return exit_code # type: ignore # mypy cannot yet infer it
if __name__ == "__main__":

View File

@ -2,11 +2,13 @@
import asyncio
import logging
from collections import OrderedDict
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple, cast
import jwt
from homeassistant import data_entry_flow
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util
@ -242,8 +244,12 @@ class AuthManager:
modules[module_id] = module.name
return modules
async def async_create_refresh_token(self, user: models.User,
client_id: Optional[str] = None) \
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
@ -254,10 +260,36 @@ class AuthManager:
'System generated users cannot have refresh tokens connected '
'to a client.')
if not user.system_generated and client_id is None:
if token_type is None:
if user.system_generated:
token_type = models.TOKEN_TYPE_SYSTEM
else:
token_type = models.TOKEN_TYPE_NORMAL
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError(
'System generated users can only have system type '
'refresh tokens')
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
raise ValueError('Client is required to generate a refresh token.')
return await self._store.async_create_refresh_token(user, client_id)
if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and
client_name is None):
raise ValueError('Client_name is required for long-lived access '
'token')
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
for token in user.refresh_tokens.values():
if (token.client_name == client_name and token.token_type ==
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN):
# Each client_name can only have one
# long_lived_access_token type of refresh token
raise ValueError('{} already exists'.format(client_name))
return await self._store.async_create_refresh_token(
user, client_id, client_name, client_icon,
token_type, access_token_expiration)
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
@ -277,13 +309,17 @@ class AuthManager:
@callback
def async_create_access_token(self,
refresh_token: models.RefreshToken) -> str:
refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> str:
"""Create a new access token."""
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
# pylint: disable=no-self-use
now = dt_util.utcnow()
return jwt.encode({
'iss': refresh_token.id,
'iat': dt_util.utcnow(),
'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
'iat': now,
'exp': now + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode()
async def async_validate_access_token(

View File

@ -5,6 +5,7 @@ from logging import getLogger
from typing import Any, Dict, List, Optional # noqa: F401
import hmac
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util
@ -27,7 +28,8 @@ class AuthStore:
"""Initialize the auth store."""
self.hass = hass
self._users = None # type: Optional[Dict[str, models.User]]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True)
async def async_get_users(self) -> List[models.User]:
"""Retrieve all users."""
@ -128,11 +130,27 @@ class AuthStore:
self._async_schedule_save()
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None) \
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
"""Create a new token for a user."""
refresh_token = models.RefreshToken(user=user, client_id=client_id)
kwargs = {
'user': user,
'client_id': client_id,
'token_type': token_type,
'access_token_expiration': access_token_expiration
} # type: Dict[str, Any]
if client_name:
kwargs['client_name'] = client_name
if client_icon:
kwargs['client_icon'] = client_icon
refresh_token = models.RefreshToken(**kwargs)
user.refresh_tokens[refresh_token.id] = refresh_token
self._async_schedule_save()
return refresh_token
@ -178,6 +196,15 @@ class AuthStore:
return found
@callback
def async_log_refresh_token_usage(
self, refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> None:
"""Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow()
refresh_token.last_used_ip = remote_ip
self._async_schedule_save()
async def _async_load(self) -> None:
"""Load the users."""
data = await self._store.async_load()
@ -216,15 +243,36 @@ class AuthStore:
'Ignoring refresh token %(id)s with invalid created_at '
'%(created_at)s for user_id %(user_id)s', rt_dict)
continue
token_type = rt_dict.get('token_type')
if token_type is None:
if rt_dict['client_id'] is None:
token_type = models.TOKEN_TYPE_SYSTEM
else:
token_type = models.TOKEN_TYPE_NORMAL
# old refresh_token don't have last_used_at (pre-0.78)
last_used_at_str = rt_dict.get('last_used_at')
if last_used_at_str:
last_used_at = dt_util.parse_datetime(last_used_at_str)
else:
last_used_at = None
token = models.RefreshToken(
id=rt_dict['id'],
user=users[rt_dict['user_id']],
client_id=rt_dict['client_id'],
# use dict.get to keep backward compatibility
client_name=rt_dict.get('client_name'),
client_icon=rt_dict.get('client_icon'),
token_type=token_type,
created_at=created_at,
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
jwt_key=rt_dict['jwt_key']
jwt_key=rt_dict['jwt_key'],
last_used_at=last_used_at,
last_used_ip=rt_dict.get('last_used_ip'),
)
users[rt_dict['user_id']].refresh_tokens[token.id] = token
@ -271,11 +319,18 @@ class AuthStore:
'id': refresh_token.id,
'user_id': user.id,
'client_id': refresh_token.client_id,
'client_name': refresh_token.client_name,
'client_icon': refresh_token.client_icon,
'token_type': refresh_token.token_type,
'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
'jwt_key': refresh_token.jwt_key,
'last_used_at':
refresh_token.last_used_at.isoformat()
if refresh_token.last_used_at else None,
'last_used_ip': refresh_token.last_used_ip,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()

View File

@ -2,3 +2,4 @@
from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
MFA_SESSION_EXPIRATION = timedelta(minutes=5)

View File

@ -1,5 +1,4 @@
"""Plugable auth modules for Home Assistant."""
from datetime import timedelta
import importlib
import logging
import types
@ -23,8 +22,6 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
SESSION_EXPIRATION = timedelta(minutes=5)
DATA_REQS = 'mfa_auth_module_reqs_processed'
_LOGGER = logging.getLogger(__name__)
@ -34,6 +31,7 @@ class MultiFactorAuthModule:
"""Multi-factor Auth Module of validation function."""
DEFAULT_TITLE = 'Unnamed auth module'
MAX_RETRY_TIME = 3
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize an auth module."""
@ -84,7 +82,7 @@ class MultiFactorAuthModule:
"""Return whether user is setup."""
raise NotImplementedError
async def async_validation(
async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
raise NotImplementedError

View File

@ -77,7 +77,7 @@ class InsecureExampleModule(MultiFactorAuthModule):
return True
return False
async def async_validation(
async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
for data in self._data:

View File

@ -0,0 +1,325 @@
"""HMAC-based One-time Password auth module.
Sending HOTP through notify service
"""
import logging
from collections import OrderedDict
from typing import Any, Dict, Optional, Tuple, List # noqa: F401
import attr
import voluptuous as vol
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
REQUIREMENTS = ['pyotp==2.2.6']
CONF_MESSAGE = 'message'
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_MESSAGE,
default='{} is your Home Assistant login code'): str
}, extra=vol.PREVENT_EXTRA)
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_module.notify'
STORAGE_USERS = 'users'
STORAGE_USER_ID = 'user_id'
INPUT_FIELD_CODE = 'code'
_LOGGER = logging.getLogger(__name__)
def _generate_secret() -> str:
"""Generate a secret."""
import pyotp
return str(pyotp.random_base32())
def _generate_random() -> int:
"""Generate a 8 digit number."""
import pyotp
return int(pyotp.random_base32(length=8, chars=list('1234567890')))
def _generate_otp(secret: str, count: int) -> str:
"""Generate one time password."""
import pyotp
return str(pyotp.HOTP(secret).at(count))
def _verify_otp(secret: str, otp: str, count: int) -> bool:
"""Verify one time password."""
import pyotp
return bool(pyotp.HOTP(secret).verify(otp, count))
@attr.s(slots=True)
class NotifySetting:
"""Store notify setting for one user."""
secret = attr.ib(type=str, factory=_generate_secret) # not persistent
counter = attr.ib(type=int, factory=_generate_random) # not persistent
notify_service = attr.ib(type=Optional[str], default=None)
target = attr.ib(type=Optional[str], default=None)
_UsersDict = Dict[str, NotifySetting]
@MULTI_FACTOR_AUTH_MODULES.register('notify')
class NotifyAuthModule(MultiFactorAuthModule):
"""Auth module send hmac-based one time password by notify service."""
DEFAULT_TITLE = 'Notify One-Time Password'
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._user_settings = None # type: Optional[_UsersDict]
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True)
self._include = config.get(CONF_INCLUDE, [])
self._exclude = config.get(CONF_EXCLUDE, [])
self._message_template = config[CONF_MESSAGE]
@property
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({INPUT_FIELD_CODE: str})
async def _async_load(self) -> None:
"""Load stored data."""
data = await self._user_store.async_load()
if data is None:
data = {STORAGE_USERS: {}}
self._user_settings = {
user_id: NotifySetting(**setting)
for user_id, setting in data.get(STORAGE_USERS, {}).items()
}
async def _async_save(self) -> None:
"""Save data."""
if self._user_settings is None:
return
await self._user_store.async_save({STORAGE_USERS: {
user_id: attr.asdict(
notify_setting, filter=attr.filters.exclude(
attr.fields(NotifySetting).secret,
attr.fields(NotifySetting).counter,
))
for user_id, notify_setting
in self._user_settings.items()
}})
@callback
def aync_get_available_notify_services(self) -> List[str]:
"""Return list of notify services."""
unordered_services = set()
for service in self.hass.services.async_services().get('notify', {}):
if service not in self._exclude:
unordered_services.add(service)
if self._include:
unordered_services &= set(self._include)
return sorted(unordered_services)
async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module.
Mfa module should extend SetupFlow
"""
return NotifySetupFlow(
self, self.input_schema, user_id,
self.aync_get_available_notify_services())
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up auth module for user."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
self._user_settings[user_id] = NotifySetting(
notify_service=setup_data.get('notify_service'),
target=setup_data.get('target'),
)
await self._async_save()
async def async_depose_user(self, user_id: str) -> None:
"""Depose auth module for user."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
if self._user_settings.pop(user_id, None):
await self._async_save()
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
return user_id in self._user_settings
async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
notify_setting = self._user_settings.get(user_id, None)
if notify_setting is None:
return False
# user_input has been validate in caller
return await self.hass.async_add_executor_job(
_verify_otp, notify_setting.secret,
user_input.get(INPUT_FIELD_CODE, ''),
notify_setting.counter)
async def async_initialize_login_mfa_step(self, user_id: str) -> None:
"""Generate code and notify user."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
notify_setting = self._user_settings.get(user_id, None)
if notify_setting is None:
raise ValueError('Cannot find user_id')
def generate_secret_and_one_time_password() -> str:
"""Generate and send one time password."""
assert notify_setting
# secret and counter are not persistent
notify_setting.secret = _generate_secret()
notify_setting.counter = _generate_random()
return _generate_otp(
notify_setting.secret, notify_setting.counter)
code = await self.hass.async_add_executor_job(
generate_secret_and_one_time_password)
await self.async_notify_user(user_id, code)
async def async_notify_user(self, user_id: str, code: str) -> None:
"""Send code by user's notify service."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
notify_setting = self._user_settings.get(user_id, None)
if notify_setting is None:
_LOGGER.error('Cannot find user %s', user_id)
return
await self.async_notify( # type: ignore
code, notify_setting.notify_service, notify_setting.target)
async def async_notify(self, code: str, notify_service: str,
target: Optional[str] = None) -> None:
"""Send code by notify service."""
data = {'message': self._message_template.format(code)}
if target:
data['target'] = [target]
await self.hass.services.async_call('notify', notify_service, data)
class NotifySetupFlow(SetupFlow):
"""Handler for the setup flow."""
def __init__(self, auth_module: NotifyAuthModule,
setup_schema: vol.Schema,
user_id: str,
available_notify_services: List[str]) -> None:
"""Initialize the setup flow."""
super().__init__(auth_module, setup_schema, user_id)
# to fix typing complaint
self._auth_module = auth_module # type: NotifyAuthModule
self._available_notify_services = available_notify_services
self._secret = None # type: Optional[str]
self._count = None # type: Optional[int]
self._notify_service = None # type: Optional[str]
self._target = None # type: Optional[str]
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Let user select available notify services."""
errors = {} # type: Dict[str, str]
hass = self._auth_module.hass
if user_input:
self._notify_service = user_input['notify_service']
self._target = user_input.get('target')
self._secret = await hass.async_add_executor_job(_generate_secret)
self._count = await hass.async_add_executor_job(_generate_random)
return await self.async_step_setup()
if not self._available_notify_services:
return self.async_abort(reason='no_available_service')
schema = OrderedDict() # type: Dict[str, Any]
schema['notify_service'] = vol.In(self._available_notify_services)
schema['target'] = vol.Optional(str)
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors
)
async def async_step_setup(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Verify user can recevie one-time password."""
errors = {} # type: Dict[str, str]
hass = self._auth_module.hass
if user_input:
verified = await hass.async_add_executor_job(
_verify_otp, self._secret, user_input['code'], self._count)
if verified:
await self._auth_module.async_setup_user(
self._user_id, {
'notify_service': self._notify_service,
'target': self._target,
})
return self.async_create_entry(
title=self._auth_module.name,
data={}
)
errors['base'] = 'invalid_code'
# generate code every time, no retry logic
assert self._secret and self._count
code = await hass.async_add_executor_job(
_generate_otp, self._secret, self._count)
assert self._notify_service
await self._auth_module.async_notify(
code, self._notify_service, self._target)
return self.async_show_form(
step_id='setup',
data_schema=self._setup_schema,
description_placeholders={'notify_service': self._notify_service},
errors=errors,
)

View File

@ -60,13 +60,14 @@ class TotpAuthModule(MultiFactorAuthModule):
"""Auth module validate time-based one time password."""
DEFAULT_TITLE = 'Time-based One Time Password'
MAX_RETRY_TIME = 5
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._users = None # type: Optional[Dict[str, str]]
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY)
STORAGE_VERSION, STORAGE_KEY, private=True)
@property
def input_schema(self) -> vol.Schema:
@ -130,7 +131,7 @@ class TotpAuthModule(MultiFactorAuthModule):
return user_id in self._users # type: ignore
async def async_validation(
async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._users is None:
@ -149,10 +150,10 @@ class TotpAuthModule(MultiFactorAuthModule):
if ota_secret is None:
# even we cannot find user, we still do verify
# to make timing the same as if user was found.
pyotp.TOTP(DUMMY_SECRET).verify(code)
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
return False
return bool(pyotp.TOTP(ota_secret).verify(code))
return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1))
class TotpSetupFlow(SetupFlow):

View File

@ -7,9 +7,12 @@ import attr
from homeassistant.util import dt as dt_util
from .const import ACCESS_TOKEN_EXPIRATION
from .util import generate_secret
TOKEN_TYPE_NORMAL = 'normal'
TOKEN_TYPE_SYSTEM = 'system'
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
@attr.s(slots=True)
class User:
@ -37,23 +40,31 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User)
client_id = attr.ib(type=str) # type: Optional[str]
client_id = attr.ib(type=Optional[str])
access_token_expiration = attr.ib(type=timedelta)
client_name = attr.ib(type=Optional[str], default=None)
client_icon = attr.ib(type=Optional[str], default=None)
token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_((
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
access_token_expiration = attr.ib(type=timedelta,
default=ACCESS_TOKEN_EXPIRATION)
token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
jwt_key = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64)))
last_used_at = attr.ib(type=Optional[datetime], default=None)
last_used_ip = attr.ib(type=Optional[str], default=None)
@attr.s(slots=True)
class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=str) # type: Optional[str]
auth_provider_id = attr.ib(type=Optional[str])
# Allow the auth provider to store data to represent their auth.
data = attr.ib(type=dict)

View File

@ -15,8 +15,8 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry
from ..auth_store import AuthStore
from ..const import MFA_SESSION_EXPIRATION
from ..models import Credentials, User, UserMeta # noqa: F401
from ..mfa_modules import SESSION_EXPIRATION
_LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed'
@ -171,6 +171,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
self._auth_manager = auth_provider.hass.auth # type: ignore
self.available_mfa_modules = {} # type: Dict[str, str]
self.created_at = dt_util.utcnow()
self.invalid_mfa_times = 0
self.user = None # type: Optional[User]
async def async_step_init(
@ -212,6 +213,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of mfa validation."""
assert self.user
errors = {}
auth_module = self._auth_manager.get_auth_mfa_module(
@ -221,25 +224,34 @@ class LoginFlow(data_entry_flow.FlowHandler):
# will show invalid_auth_module error
return await self.async_step_select_mfa_module(user_input={})
if user_input is None and hasattr(auth_module,
'async_initialize_login_mfa_step'):
await auth_module.async_initialize_login_mfa_step(self.user.id)
if user_input is not None:
expires = self.created_at + SESSION_EXPIRATION
expires = self.created_at + MFA_SESSION_EXPIRATION
if dt_util.utcnow() > expires:
return self.async_abort(
reason='login_expired'
)
result = await auth_module.async_validation(
self.user.id, user_input) # type: ignore
result = await auth_module.async_validate(
self.user.id, user_input)
if not result:
errors['base'] = 'invalid_code'
self.invalid_mfa_times += 1
if self.invalid_mfa_times >= auth_module.MAX_RETRY_TIME > 0:
return self.async_abort(
reason='too_many_retry'
)
if not errors:
return await self.async_finish(self.user)
description_placeholders = {
'mfa_module_name': auth_module.name,
'mfa_module_id': auth_module.id
} # type: Dict[str, str]
'mfa_module_id': auth_module.id,
} # type: Dict[str, Optional[str]]
return self.async_show_form(
step_id='mfa',

View File

@ -52,7 +52,8 @@ class Data:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the user data store."""
self.hass = hass
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True)
self._data = None # type: Optional[Dict[str, Any]]
async def async_load(self) -> None:

View File

@ -24,7 +24,7 @@ USER_SCHEMA = vol.Schema({
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
LEGACY_USER = 'homeassistant'
LEGACY_USER_NAME = 'Legacy API password user'
class InvalidAuthError(HomeAssistantError):
@ -52,23 +52,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Return LEGACY_USER always."""
for credential in await self.async_credentials():
if credential.data['username'] == LEGACY_USER:
return credential
"""Return credentials for this login."""
credentials = await self.async_credentials()
if credentials:
return credentials[0]
return self.async_create_credentials({
'username': LEGACY_USER
})
return self.async_create_credentials({})
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""
Set name as LEGACY_USER always.
Return info for the user.
Will be used to populate info when creating a new user.
"""
return UserMeta(name=LEGACY_USER, is_active=True)
return UserMeta(name=LEGACY_USER_NAME, is_active=True)
class LegacyLoginFlow(LoginFlow):

View File

@ -5,7 +5,6 @@ import os
import sys
from time import time
from collections import OrderedDict
from typing import Any, Optional, Dict
import voluptuous as vol
@ -19,7 +18,6 @@ from homeassistant.util.logging import AsyncHandler
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.signal import async_register_signal_handling
_LOGGER = logging.getLogger(__name__)
@ -160,7 +158,6 @@ async def async_from_config_dict(config: Dict[str, Any],
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
async_register_signal_handling(hass)
return hass

View File

@ -14,7 +14,6 @@ from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
from homeassistant.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@ -32,78 +31,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
})
@bind_hass
def alarm_disarm(hass, code=None, entity_id=None):
"""Send the alarm the command for disarm."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data)
@bind_hass
def alarm_arm_home(hass, code=None, entity_id=None):
"""Send the alarm the command for arm home."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data)
@bind_hass
def alarm_arm_away(hass, code=None, entity_id=None):
"""Send the alarm the command for arm away."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
@bind_hass
def alarm_arm_night(hass, code=None, entity_id=None):
"""Send the alarm the command for arm night."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data)
@bind_hass
def alarm_trigger(hass, code=None, entity_id=None):
"""Send the alarm the command for trigger."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
@bind_hass
def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
"""Send the alarm the command for arm custom bypass."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data)
@asyncio.coroutine
def async_setup(hass, config):
"""Track states and offer events for sensors."""

View File

@ -18,10 +18,13 @@ from homeassistant.const import (
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
CONF_NAME, CONF_CODE)
from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
CONF_RETAIN, MqttAvailability)
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__)
@ -46,13 +49,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the MQTT Alarm Control Panel platform."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None):
"""Set up MQTT alarm control panel through configuration.yaml."""
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT alarm control panel dynamically through MQTT discovery."""
async def async_discover(discovery_payload):
"""Discover and add an MQTT alarm control panel."""
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(hass, config, async_add_entities,
discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(alarm.DOMAIN, 'mqtt'),
async_discover)
async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None):
"""Set up the MQTT Alarm Control Panel platform."""
async_add_entities([MqttAlarm(
config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC),
@ -65,18 +83,22 @@ def async_setup_platform(hass, config, async_add_entities,
config.get(CONF_CODE),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
discovery_hash,)])
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, retain,
payload_disarm, payload_arm_home, payload_arm_away, code,
availability_topic, payload_available, payload_not_available):
availability_topic, payload_available, payload_not_available,
discovery_hash):
"""Init the MQTT Alarm Control Panel."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
self._state = STATE_UNKNOWN
self._name = name
self._state_topic = state_topic
@ -87,11 +109,13 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
self._payload_arm_home = payload_arm_home
self._payload_arm_away = payload_arm_away
self._code = code
self._discovery_hash = discovery_hash
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe mqtt events."""
yield from super().async_added_to_hass()
yield from MqttAvailability.async_added_to_hass(self)
yield from MqttDiscoveryUpdate.async_added_to_hass(self)
@callback
def message_received(topic, payload, qos):

View File

@ -4,71 +4,65 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.spc/
"""
import asyncio
import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
from homeassistant.components.spc import (
ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
ATTR_DISCOVER_AREAS, DATA_API, SIGNAL_UPDATE_ALARM)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN)
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__)
SPC_AREA_MODE_TO_STATE = {
'0': STATE_ALARM_DISARMED,
'1': STATE_ALARM_ARMED_HOME,
'3': STATE_ALARM_ARMED_AWAY,
}
def _get_alarm_state(spc_mode):
def _get_alarm_state(area):
"""Get the alarm state."""
return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN)
from pyspcwebgw.const import AreaMode
if area.verified_alarm:
return STATE_ALARM_TRIGGERED
mode_to_state = {
AreaMode.UNSET: STATE_ALARM_DISARMED,
AreaMode.PART_SET_A: STATE_ALARM_ARMED_HOME,
AreaMode.PART_SET_B: STATE_ALARM_ARMED_NIGHT,
AreaMode.FULL_SET: STATE_ALARM_ARMED_AWAY,
}
return mode_to_state.get(area.mode)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the SPC alarm control panel platform."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_AREAS] is None):
return
api = hass.data[DATA_API]
devices = [SpcAlarm(api, area)
for area in discovery_info[ATTR_DISCOVER_AREAS]]
async_add_entities(devices)
async_add_entities([SpcAlarm(area=area, api=hass.data[DATA_API])
for area in discovery_info[ATTR_DISCOVER_AREAS]])
class SpcAlarm(alarm.AlarmControlPanel):
"""Representation of the SPC alarm panel."""
def __init__(self, api, area):
def __init__(self, area, api):
"""Initialize the SPC alarm panel."""
self._area_id = area['id']
self._name = area['name']
self._state = _get_alarm_state(area['mode'])
if self._state == STATE_ALARM_DISARMED:
self._changed_by = area.get('last_unset_user_name', 'unknown')
else:
self._changed_by = area.get('last_set_user_name', 'unknown')
self._area = area
self._api = api
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Call for adding new entities."""
self.hass.data[DATA_REGISTRY].register_alarm_device(
self._area_id, self)
async_dispatcher_connect(self.hass,
SIGNAL_UPDATE_ALARM.format(self._area.id),
self._update_callback)
@asyncio.coroutine
def async_update_from_spc(self, state, extra):
"""Update the alarm panel with a new state."""
self._state = state
self._changed_by = extra.get('changed_by', 'unknown')
self.async_schedule_update_ha_state()
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def should_poll(self):
@ -78,32 +72,34 @@ class SpcAlarm(alarm.AlarmControlPanel):
@property
def name(self):
"""Return the name of the device."""
return self._name
return self._area.name
@property
def changed_by(self):
"""Return the user the last change was triggered by."""
return self._changed_by
return self._area.last_changed_by
@property
def state(self):
"""Return the state of the device."""
return self._state
return _get_alarm_state(self._area)
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
from pyspcwebgw.const import AreaMode
self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET)
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
from pyspcwebgw.const import AreaMode
self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A)
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
async def async_alarm_arm_night(self, code=None):
"""Send arm home command."""
from pyspcwebgw.const import AreaMode
self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B)
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
from pyspcwebgw.const import AreaMode
self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET)

View File

@ -0,0 +1,98 @@
"""
Yale Smart Alarm client for interacting with the Yale Smart Alarm System API.
For more details about this platform, please refer to the documentation at
https://www.home-assistant.io/components/alarm_control_panel.yale_smart_alarm
"""
import logging
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['yalesmartalarmclient==0.1.4']
CONF_AREA_ID = 'area_id'
DEFAULT_NAME = 'Yale Smart Alarm'
DEFAULT_AREA_ID = '1'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the alarm platform."""
name = config[CONF_NAME]
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
area_id = config[CONF_AREA_ID]
from yalesmartalarmclient.client import (
YaleSmartAlarmClient, AuthenticationError)
try:
client = YaleSmartAlarmClient(username, password, area_id)
except AuthenticationError:
_LOGGER.error("Authentication failed. Check credentials")
return
add_entities([YaleAlarmDevice(name, client)], True)
class YaleAlarmDevice(AlarmControlPanel):
"""Represent a Yale Smart Alarm."""
def __init__(self, name, client):
"""Initialize the Yale Alarm Device."""
self._name = name
self._client = client
self._state = None
from yalesmartalarmclient.client import (YALE_STATE_DISARM,
YALE_STATE_ARM_PARTIAL,
YALE_STATE_ARM_FULL)
self._state_map = {
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY
}
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Return the state of the device."""
armed_status = self._client.get_armed_status()
self._state = self._state_map.get(armed_status)
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._client.disarm()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._client.arm_partial()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._client.arm_full()

View File

@ -1529,3 +1529,8 @@ async def async_api_reportstate(hass, config, request, context, entity):
name='StateReport',
context={'properties': properties}
)
def turned_off_response(message):
"""Return a device turned off response."""
return api_error(message[API_DIRECTIVE], error_type='BRIDGE_UNREACHABLE')

View File

@ -6,7 +6,6 @@ https://home-assistant.io/components/apple_tv/
"""
import asyncio
import logging
from typing import Sequence, TypeVar, Union
import voluptuous as vol

View File

@ -61,10 +61,12 @@ def setup(hass, config):
arlo_base_station = next((
station for station in arlo.base_stations), None)
if arlo_base_station is None:
if arlo_base_station is not None:
arlo_base_station.refresh_rate = scan_interval.total_seconds()
elif not arlo.cameras:
_LOGGER.error("No Arlo camera or base station available.")
return False
arlo_base_station.refresh_rate = scan_interval.total_seconds()
hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex:

View File

@ -13,7 +13,7 @@ from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
async_dispatcher_send, dispatcher_connect)
REQUIREMENTS = ['asterisk_mbox==0.5.0']
@ -21,8 +21,11 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'asterisk_mbox'
SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform"
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated'
SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -41,9 +44,7 @@ def setup(hass, config):
port = conf.get(CONF_PORT)
password = conf.get(CONF_PASSWORD)
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config)
return True
@ -51,31 +52,71 @@ def setup(hass, config):
class AsteriskData:
"""Store Asterisk mailbox data."""
def __init__(self, hass, host, port, password):
def __init__(self, hass, host, port, password, config):
"""Init the Asterisk data object."""
from asterisk_mbox import Client as asteriskClient
self.hass = hass
self.client = asteriskClient(host, port, password, self.handle_data)
self.messages = []
self.config = config
self.messages = None
self.cdr = None
async_dispatcher_connect(
dispatcher_connect(
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
dispatcher_connect(
self.hass, SIGNAL_CDR_REQUEST, self._request_cdr)
dispatcher_connect(
self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform)
# Only connect after signal connection to ensure we don't miss any
self.client = asteriskClient(host, port, password, self.handle_data)
@callback
def _discover_platform(self, component):
_LOGGER.debug("Adding mailbox %s", component)
self.hass.async_create_task(discovery.async_load_platform(
self.hass, "mailbox", component, {}, self.config))
@callback
def handle_data(self, command, msg):
"""Handle changes to the mailbox."""
from asterisk_mbox.commands import CMD_MESSAGE_LIST
from asterisk_mbox.commands import (CMD_MESSAGE_LIST,
CMD_MESSAGE_CDR_AVAILABLE,
CMD_MESSAGE_CDR)
if command == CMD_MESSAGE_LIST:
_LOGGER.debug("AsteriskVM sent updated message list")
_LOGGER.debug("AsteriskVM sent updated message list: Len %d",
len(msg))
old_messages = self.messages
self.messages = sorted(
msg, key=lambda item: item['info']['origtime'], reverse=True)
async_dispatcher_send(
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
if not isinstance(old_messages, list):
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
DOMAIN)
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE,
self.messages)
elif command == CMD_MESSAGE_CDR:
_LOGGER.debug("AsteriskVM sent updated CDR list: Len %d",
len(msg.get('entries', [])))
self.cdr = msg['entries']
async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr)
elif command == CMD_MESSAGE_CDR_AVAILABLE:
if not isinstance(self.cdr, list):
_LOGGER.debug("AsteriskVM adding CDR platform")
self.cdr = []
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
"asterisk_cdr")
async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST)
else:
_LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d",
command, len(msg))
@callback
def _request_messages(self):
"""Handle changes to the mailbox."""
_LOGGER.debug("Requesting message list")
self.client.messages()
@callback
def _request_cdr(self):
"""Handle changes to the CDR."""
_LOGGER.debug("Requesting CDR list")
self.client.get_cdr()

View File

@ -1,8 +1,27 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles."
},
"error": {
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho."
},
"step": {
"init": {
"description": "Seleccioneu un dels serveis de notificaci\u00f3:",
"title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
},
"setup": {
"description": "**notify.{notify_service}** ha enviat una contrasenya d'un sol \u00fas. Introdu\u00efu-la a continuaci\u00f3:",
"title": "Verifiqueu la configuraci\u00f3"
}
},
"title": "Contrasenya d'un sol \u00fas del servei de notificacions"
},
"totp": {
"error": {
"invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
},
"step": {
"init": {

View File

@ -1,8 +1,26 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Keine Benachrichtigungsdienste verf\u00fcgbar."
},
"error": {
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut."
},
"step": {
"init": {
"description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:",
"title": "Einmal Passwort f\u00fcr Notify einrichten"
},
"setup": {
"description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:",
"title": "\u00dcberpr\u00fcfe das Setup"
}
}
},
"totp": {
"error": {
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn Sie diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn du diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
},
"step": {
"init": {

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "No notification services available."
},
"error": {
"invalid_code": "Invalid code, please try again."
},
"step": {
"init": {
"description": "Please select one of the notification services:",
"title": "Set up one-time password delivered by notify component"
},
"setup": {
"description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:",
"title": "Verify setup"
}
},
"title": "Notify One-Time Password"
},
"totp": {
"error": {
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."

View File

@ -1,8 +1,15 @@
{
"mfa_setup": {
"notify": {
"step": {
"setup": {
"description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :"
}
}
},
"totp": {
"error": {
"invalid_code": "Code invalide. S'il vous pla\u00eet essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte."
"invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte."
},
"step": {
"init": {

View File

@ -0,0 +1,35 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 notify \u05d6\u05de\u05d9\u05e0\u05d9\u05dd."
},
"error": {
"invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1."
},
"step": {
"init": {
"description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d0\u05d7\u05d3 \u05de\u05e9\u05e8\u05d5\u05ea\u05d9 notify",
"title": "\u05d4\u05d2\u05d3\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05d4\u05e0\u05e9\u05dc\u05d7\u05ea \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05db\u05d9\u05d1 notify"
},
"setup": {
"description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 **{notify_service}**. \u05d4\u05d6\u05df \u05d0\u05d5\u05ea\u05d4 \u05dc\u05de\u05d8\u05d4:",
"title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4"
}
},
"title": "\u05dc\u05d4\u05d5\u05d3\u05d9\u05e2 \u200b\u200b\u05e2\u05dc \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea"
},
"totp": {
"error": {
"invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e7\u05d1\u05dc \u05d0\u05ea \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d4\u05d6\u05d5 \u05d1\u05d0\u05d5\u05e4\u05df \u05e2\u05e7\u05d1\u05d9, \u05d5\u05d3\u05d0 \u05e9\u05d4\u05e9\u05e2\u05d5\u05df \u05e9\u05dc \u05de\u05e2\u05e8\u05db\u05ea \u05d4 - Home Assistant \u05e9\u05dc\u05da \u05de\u05d3\u05d5\u05d9\u05e7."
},
"step": {
"init": {
"description": "\u05db\u05d3\u05d9 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d0\u05d5\u05ea \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05d5\u05ea \u05de\u05d1\u05d5\u05e1\u05e1\u05d5\u05ea \u05d6\u05de\u05df, \u05e1\u05e8\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 QR \u05e2\u05dd \u05d9\u05d9\u05e9\u05d5\u05dd \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05da. \u05d0\u05dd \u05d0\u05d9\u05df \u05dc\u05da \u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d6\u05d4, \u05d0\u05e0\u05d5 \u05de\u05de\u05dc\u05d9\u05e6\u05d9\u05dd \u05e2\u05dc [Google Authenticator] (https://support.google.com/accounts/answer/1066447) \u05d0\u05d5 [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u05dc\u05d0\u05d7\u05e8 \u05e1\u05e8\u05d9\u05e7\u05ea \u05d4\u05e7\u05d5\u05d3, \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e7\u05d5\u05d3 \u05d1\u05df \u05e9\u05e9 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05de\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d4. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e1\u05e8\u05d9\u05e7\u05ea \u05e7\u05d5\u05d3 QR, \u05d1\u05e6\u05e2 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d3\u05e0\u05d9\u05ea \u05e2\u05dd \u05e7\u05d5\u05d3 **`{code}`**.",
"title": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r."
},
"step": {
"init": {
"description": "Ahhoz, hogy haszn\u00e1lhasd a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkenneld be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3ddal. Ha m\u00e9g nincsen, akkor a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t aj\u00e1nljuk.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n add meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6z\u00f6l a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edts egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.",
"title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val"
}
},
"title": "TOTP"
}
}
}

View File

@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Kode salah, coba lagi. Jika Anda mendapatkan kesalahan ini secara konsisten, pastikan jam pada sistem Home Assistant anda akurat."
},
"step": {
"init": {
"description": "Untuk mengaktifkan otentikasi dua faktor menggunakan password satu kali berbasis waktu, pindai kode QR dengan aplikasi otentikasi Anda. Jika Anda tidak memilikinya, kami menyarankan [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \n Setelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi pengaturan. Jika Anda mengalami masalah saat memindai kode QR, lakukan pengaturan manual dengan kode ** ` {code} ` **.",
"title": "Siapkan otentikasi dua faktor menggunakan TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -1,12 +1,31 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"error": {
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
},
"step": {
"init": {
"description": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:",
"title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815"
},
"setup": {
"description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574 \uc8fc\uc138\uc694:",
"title": "\uc124\uc815 \ud655\uc778"
}
},
"title": "\uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc54c\ub9bc"
},
"totp": {
"error": {
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694."
},
"step": {
"init": {
"description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
"description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
"title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
}
},

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Keen Notifikatioun's D\u00e9ngscht disponibel."
},
"error": {
"invalid_code": "Ong\u00ebltege Code, prob\u00e9iert w.e.g. nach emol."
},
"step": {
"init": {
"description": "Wielt w.e.g. een Notifikatioun's D\u00e9ngscht aus:",
"title": "Eemolegt Passwuert ariichte wat vun engem Notifikatioun's Komponente versch\u00e9ckt g\u00ebtt"
},
"setup": {
"description": "Een eemolegt Passwuert ass vun **notify.{notify_service}** gesch\u00e9ckt ginn. Gitt et w.e.g hei \u00ebnnen dr\u00ebnner an:",
"title": "Astellungen iwwerpr\u00e9iwen"
}
},
"title": "Eemolegt Passwuert Notifikatioun"
},
"totp": {
"error": {
"invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass."

View File

@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Ugyldig kode, pr\u00f8v igjen. Dersom du heile tida f\u00e5r denne feilen, m\u00e5 du s\u00f8rge for at klokka p\u00e5 Home Assistant-systemet ditt er n\u00f8yaktig."
},
"step": {
"init": {
"description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte eingangspassord, skann QR-koden med autentiseringsappen din. Dersom du ikkje har ein, vil vi r\u00e5de deg til \u00e5 bruke anten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har skanna koda, skriv du inn den sekssifra koda fr\u00e5 appen din for \u00e5 stadfeste oppsettet. Dersom du har problemer med \u00e5 skanne QR-koda, gjer du eit manuelt oppsett med kode ** ` {code} ` **.",
"title": "Konfigurer to-faktor-autentisering ved hjelp av TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Ingen varslingstjenester er tilgjengelig."
},
"error": {
"invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen."
},
"step": {
"init": {
"description": "Vennligst velg en av varslingstjenestene:",
"title": "Sett opp engangspassord levert av varsel komponent"
},
"setup": {
"description": "Et engangspassord har blitt sendt via **notify.{notify_service}**. Vennligst skriv det inn nedenfor:",
"title": "Bekreft oppsettet"
}
},
"title": "Varsle engangspassord"
},
"totp": {
"error": {
"invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig."

View File

@ -1,5 +1,23 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Brak dost\u0119pnych us\u0142ug powiadamiania."
},
"error": {
"invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie."
},
"step": {
"init": {
"description": "Prosz\u0119 wybra\u0107 jedn\u0105 z us\u0142ugi powiadamiania:",
"title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144"
},
"setup": {
"description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez ** powiadom. {notify_service} **. Wpisz je poni\u017cej:",
"title": "Sprawd\u017a konfiguracj\u0119"
}
}
},
"totp": {
"error": {
"invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy."

View File

@ -0,0 +1,15 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente. Se voc\u00ea obtiver este erro de forma consistente, certifique-se de que o rel\u00f3gio do sistema Home Assistant esteja correto."
},
"step": {
"init": {
"title": "Configure a autentica\u00e7\u00e3o de dois fatores usando o TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439."
},
"error": {
"invalid_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443."
},
"step": {
"init": {
"description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u0443 \u0438\u0437 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439:",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439"
},
"setup": {
"description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u0447\u0435\u0440\u0435\u0437 **notify.{notify_service}**. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u043d\u0438\u0436\u0435:",
"title": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443"
}
},
"title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439"
},
"totp": {
"error": {
"invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f."

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Ni na voljo storitev obve\u0161\u010danja."
},
"error": {
"invalid_code": "Neveljavna koda, poskusite znova."
},
"step": {
"init": {
"description": "Prosimo, izberite eno od storitev obve\u0161\u010danja:",
"title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento"
},
"setup": {
"description": "Enkratno geslo je poslal **notify.{notify_service} **. Vnesite ga spodaj:",
"title": "Preverite nastavitev"
}
},
"title": "Obvesti Enkratno Geslo"
},
"totp": {
"error": {
"invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna."

View File

@ -0,0 +1,30 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Inga tillg\u00e4ngliga meddelande tj\u00e4nster."
},
"error": {
"invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen."
},
"step": {
"setup": {
"description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:",
"title": "Verifiera installationen"
}
}
},
"totp": {
"error": {
"invalid_code": "Ogiltig kod, f\u00f6rs\u00f6k igen. Om du flera g\u00e5nger i rad f\u00e5r detta fel, se till att klockan i din Home Assistant \u00e4r korrekt inst\u00e4lld."
},
"step": {
"init": {
"description": "F\u00f6r att aktivera tv\u00e5faktorsautentisering som anv\u00e4nder tidsbaserade eng\u00e5ngsl\u00f6senord, skanna QR-koden med din autentiseringsapp. Om du inte har en, rekommenderar vi antingen [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n{qr_code} \n\nN\u00e4r du har skannat koden anger du den sexsiffriga koden fr\u00e5n din app f\u00f6r att verifiera inst\u00e4llningen. Om du har problem med att skanna QR-koden, g\u00f6r en manuell inst\u00e4llning med kod ** ` {code} ` **.",
"title": "St\u00e4ll in tv\u00e5faktorsautentisering med TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u6ca1\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52a1\u3002"
},
"error": {
"invalid_code": "\u4ee3\u7801\u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002"
},
"step": {
"init": {
"description": "\u8bf7\u4ece\u4e0b\u9762\u9009\u62e9\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a",
"title": "\u8bbe\u7f6e\u7531\u901a\u77e5\u7ec4\u4ef6\u4f20\u9012\u7684\u4e00\u6b21\u6027\u5bc6\u7801"
},
"setup": {
"description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}** \u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165\uff1a",
"title": "\u9a8c\u8bc1\u8bbe\u7f6e"
}
},
"title": "\u4e00\u6b21\u6027\u5bc6\u7801\u901a\u77e5"
},
"totp": {
"error": {
"invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002"

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u6c92\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52d9\u3002"
},
"error": {
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002"
},
"step": {
"init": {
"description": "\u8acb\u9078\u64c7\u4e00\u9805\u901a\u77e5\u670d\u52d9\uff1a",
"title": "\u8a2d\u5b9a\u4e00\u6b21\u6027\u5bc6\u78bc\u50b3\u9001\u7d44\u4ef6"
},
"setup": {
"description": "\u4e00\u6b21\u6027\u5bc6\u78bc\u5df2\u900f\u904e **notify.{notify_service}** \u50b3\u9001\u3002\u8acb\u65bc\u4e0b\u65b9\u8f38\u5165\uff1a",
"title": "\u9a57\u8b49\u8a2d\u5b9a"
}
},
"title": "\u901a\u77e5\u4e00\u6b21\u6027\u5bc6\u78bc"
},
"totp": {
"error": {
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002"

View File

@ -12,6 +12,7 @@ be in JSON as it's more readable.
Exchange the authorization code retrieved from the login flow for tokens.
{
"client_id": "https://hassbian.local:8123/",
"grant_type": "authorization_code",
"code": "411ee2f916e648d691e937ae9344681e"
}
@ -32,6 +33,7 @@ token.
Request a new access token using a refresh token.
{
"client_id": "https://hassbian.local:8123/",
"grant_type": "refresh_token",
"refresh_token": "IJKLMNOPQRST"
}
@ -55,6 +57,66 @@ ever been granted by that refresh token. Response code will ALWAYS be 200.
"action": "revoke"
}
# Websocket API
## Get current user
Send websocket command `auth/current_user` will return current user of the
active websocket connection.
{
"id": 10,
"type": "auth/current_user",
}
The result payload likes
{
"id": 10,
"type": "result",
"success": true,
"result": {
"id": "USER_ID",
"name": "John Doe",
"is_owner': true,
"credentials": [
{
"auth_provider_type": "homeassistant",
"auth_provider_id": null
}
],
"mfa_modules": [
{
"id": "totp",
"name": "TOTP",
"enabled": true,
}
]
}
}
## Create a long-lived access token
Send websocket command `auth/long_lived_access_token` will create
a long-lived access token for current user. Access token will not be saved in
Home Assistant. User need to record the token in secure place.
{
"id": 11,
"type": "auth/long_lived_access_token",
"client_name": "GPS Logger",
"lifespan": 365
}
Result will be a long-lived access token:
{
"id": 11,
"type": "result",
"success": true,
"result": "ABCDEFGH"
}
"""
import logging
import uuid
@ -63,8 +125,10 @@ from datetime import timedelta
from aiohttp import web
import voluptuous as vol
from homeassistant.auth.models import User, Credentials
from homeassistant.auth.models import User, Credentials, \
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
@ -83,6 +147,28 @@ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CURRENT_USER,
})
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token'
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
vol.Required('lifespan'): int, # days
vol.Required('client_name'): str,
vol.Optional('client_icon'): str,
})
WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens'
SCHEMA_WS_REFRESH_TOKENS = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_REFRESH_TOKENS,
})
WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token'
SCHEMA_WS_DELETE_REFRESH_TOKEN = \
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN,
vol.Required('refresh_token_id'): str,
})
RESULT_TYPE_CREDENTIALS = 'credentials'
RESULT_TYPE_USER = 'user'
@ -100,6 +186,21 @@ async def async_setup(hass, config):
WS_TYPE_CURRENT_USER, websocket_current_user,
SCHEMA_WS_CURRENT_USER
)
hass.components.websocket_api.async_register_command(
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
websocket_create_long_lived_access_token,
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN
)
hass.components.websocket_api.async_register_command(
WS_TYPE_REFRESH_TOKENS,
websocket_refresh_tokens,
SCHEMA_WS_REFRESH_TOKENS
)
hass.components.websocket_api.async_register_command(
WS_TYPE_DELETE_REFRESH_TOKEN,
websocket_delete_refresh_token,
SCHEMA_WS_DELETE_REFRESH_TOKEN
)
await login_flow.async_setup(hass, store_result)
await mfa_setup_flow.async_setup(hass)
@ -135,10 +236,12 @@ class TokenView(HomeAssistantView):
return await self._async_handle_revoke_token(hass, data)
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(hass, data)
return await self._async_handle_auth_code(
hass, data, str(request[KEY_REAL_IP]))
if grant_type == 'refresh_token':
return await self._async_handle_refresh_token(hass, data)
return await self._async_handle_refresh_token(
hass, data, str(request[KEY_REAL_IP]))
return self.json({
'error': 'unsupported_grant_type',
@ -163,7 +266,7 @@ class TokenView(HomeAssistantView):
await hass.auth.async_remove_refresh_token(refresh_token)
return web.Response(status=200)
async def _async_handle_auth_code(self, hass, data):
async def _async_handle_auth_code(self, hass, data, remote_addr):
"""Handle authorization code request."""
client_id = data.get('client_id')
if client_id is None or not indieauth.verify_client_id(client_id):
@ -199,7 +302,8 @@ class TokenView(HomeAssistantView):
refresh_token = await hass.auth.async_create_refresh_token(user,
client_id)
access_token = hass.auth.async_create_access_token(refresh_token)
access_token = hass.auth.async_create_access_token(
refresh_token, remote_addr)
return self.json({
'access_token': access_token,
@ -209,7 +313,7 @@ class TokenView(HomeAssistantView):
int(refresh_token.access_token_expiration.total_seconds()),
})
async def _async_handle_refresh_token(self, hass, data):
async def _async_handle_refresh_token(self, hass, data, remote_addr):
"""Handle authorization code request."""
client_id = data.get('client_id')
if client_id is not None and not indieauth.verify_client_id(client_id):
@ -237,7 +341,8 @@ class TokenView(HomeAssistantView):
'error': 'invalid_request',
}, status_code=400)
access_token = hass.auth.async_create_access_token(refresh_token)
access_token = hass.auth.async_create_access_token(
refresh_token, remote_addr)
return self.json({
'access_token': access_token,
@ -343,3 +448,68 @@ def websocket_current_user(
}))
hass.async_create_task(async_get_current_user(connection.user))
@websocket_api.ws_require_user()
@callback
def websocket_create_long_lived_access_token(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Create or a long-lived access token."""
async def async_create_long_lived_access_token(user):
"""Create or a long-lived access token."""
refresh_token = await hass.auth.async_create_refresh_token(
user,
client_name=msg['client_name'],
client_icon=msg.get('client_icon'),
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
access_token_expiration=timedelta(days=msg['lifespan']))
access_token = hass.auth.async_create_access_token(
refresh_token)
connection.send_message_outside(
websocket_api.result_message(msg['id'], access_token))
hass.async_create_task(
async_create_long_lived_access_token(connection.user))
@websocket_api.ws_require_user()
@callback
def websocket_refresh_tokens(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Return metadata of users refresh tokens."""
current_id = connection.request.get('refresh_token_id')
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{
'id': refresh.id,
'client_id': refresh.client_id,
'client_name': refresh.client_name,
'client_icon': refresh.client_icon,
'type': refresh.token_type,
'created_at': refresh.created_at,
'is_current': refresh.id == current_id,
'last_used_at': refresh.last_used_at,
'last_used_ip': refresh.last_used_ip,
} for refresh in connection.user.refresh_tokens.values()]))
@websocket_api.ws_require_user()
@callback
def websocket_delete_refresh_token(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
"""Handle a delete refresh token request."""
async def async_delete_refresh_token(user, refresh_token_id):
"""Delete a refresh token."""
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
if refresh_token is None:
return websocket_api.error_message(
msg['id'], 'invalid_token_id', 'Received invalid token')
await hass.auth.async_remove_refresh_token(refresh_token)
connection.send_message_outside(
websocket_api.result_message(msg['id'], {}))
hass.async_create_task(
async_delete_refresh_token(connection.user, msg['refresh_token_id']))

View File

@ -1,24 +1,13 @@
"""Helpers to resolve client ID/secret."""
import asyncio
from ipaddress import ip_address
from html.parser import HTMLParser
from ipaddress import ip_address, ip_network
from urllib.parse import urlparse, urljoin
import aiohttp
from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces
ALLOWED_IPS = (
ip_address('127.0.0.1'),
ip_address('::1'),
)
# RFC1918 - Address allocation for Private Internets
ALLOWED_NETWORKS = (
ip_network('10.0.0.0/8'),
ip_network('172.16.0.0/12'),
ip_network('192.168.0.0/16'),
)
from homeassistant.util.network import is_local
async def verify_redirect_uri(hass, client_id, redirect_uri):
@ -185,9 +174,7 @@ def _parse_client_id(client_id):
# Not an ip address
pass
if (address is None or
address in ALLOWED_IPS or
any(address in network for network in ALLOWED_NETWORKS)):
if address is None or is_local(address):
return parts
raise ValueError('Hostname should be a domain name or local IP address')

View File

@ -66,7 +66,7 @@ associate with an credential if "type" set to "link_user" in
"version": 1
}
"""
import aiohttp.web
from aiohttp import web
import voluptuous as vol
from homeassistant import data_entry_flow
@ -95,11 +95,20 @@ class AuthProvidersView(HomeAssistantView):
async def get(self, request):
"""Get available auth providers."""
hass = request.app['hass']
if not hass.components.onboarding.async_is_onboarded():
return self.json_message(
message='Onboarding not finished',
status_code=400,
message_code='onboarding_required'
)
return self.json([{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in request.app['hass'].auth.auth_providers])
} for provider in hass.auth.auth_providers])
def _prepare_result_json(result):
@ -139,7 +148,7 @@ class LoginFlowIndexView(HomeAssistantView):
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
return web.Response(status=405)
@RequestDataValidator(vol.Schema({
vol.Required('client_id'): str,
@ -217,8 +226,9 @@ class LoginFlowResourceView(HomeAssistantView):
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
# @log_invalid_auth does not work here since it returns HTTP 200
# need manually log failed login attempts
if result['errors'] is not None and \
result['errors'].get('base') == 'invalid_auth':
if (result.get('errors') is not None and
result['errors'].get('base') in ['invalid_auth',
'invalid_code']):
await process_wrong_login(request)
return self.json(_prepare_result_json(result))

View File

@ -11,6 +11,25 @@
"error": {
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
}
},
"notify": {
"title": "Notify One-Time Password",
"step": {
"init": {
"title": "Set up one-time password delivered by notify component",
"description": "Please select one of the notification services:"
},
"setup": {
"title": "Verify setup",
"description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:"
}
},
"abort": {
"no_available_service": "No notification services available."
},
"error": {
"invalid_code": "Invalid code, please try again."
}
}
}
}

View File

@ -115,70 +115,26 @@ def is_on(hass, entity_id):
return hass.states.is_state(entity_id, STATE_ON)
@bind_hass
def turn_on(hass, entity_id=None):
"""Turn on specified automation or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
@bind_hass
def turn_off(hass, entity_id=None):
"""Turn off specified automation or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
@bind_hass
def toggle(hass, entity_id=None):
"""Toggle specified automation or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
@bind_hass
def trigger(hass, entity_id=None):
"""Trigger specified automation or all."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_TRIGGER, data)
@bind_hass
def reload(hass):
"""Reload the automation from config."""
hass.services.call(DOMAIN, SERVICE_RELOAD)
@bind_hass
def async_reload(hass):
"""Reload the automation from config.
Returns a coroutine object.
"""
return hass.services.async_call(DOMAIN, SERVICE_RELOAD)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_AUTOMATIONS)
yield from _async_process_config(hass, config, component)
await _async_process_config(hass, config, component)
@asyncio.coroutine
def trigger_service_handler(service_call):
async def trigger_service_handler(service_call):
"""Handle automation triggers."""
tasks = []
for entity in component.async_extract_from_service(service_call):
tasks.append(entity.async_trigger(
service_call.data.get(ATTR_VARIABLES), True))
service_call.data.get(ATTR_VARIABLES),
skip_condition=True,
context=service_call.context))
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def turn_onoff_service_handler(service_call):
async def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls."""
tasks = []
method = 'async_{}'.format(service_call.service)
@ -186,10 +142,9 @@ def async_setup(hass, config):
tasks.append(getattr(entity, method)())
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def toggle_service_handler(service_call):
async def toggle_service_handler(service_call):
"""Handle automation toggle service calls."""
tasks = []
for entity in component.async_extract_from_service(service_call):
@ -199,15 +154,14 @@ def async_setup(hass, config):
tasks.append(entity.async_turn_on())
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
await asyncio.wait(tasks, loop=hass.loop)
@asyncio.coroutine
def reload_service_handler(service_call):
async def reload_service_handler(service_call):
"""Remove all automations and load new ones from config."""
conf = yield from component.async_prepare_reload()
conf = await component.async_prepare_reload()
if conf is None:
return
yield from _async_process_config(hass, conf, component)
await _async_process_config(hass, conf, component)
hass.services.async_register(
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
@ -272,15 +226,14 @@ class AutomationEntity(ToggleEntity):
"""Return True if entity is on."""
return self._async_detach_triggers is not None
@asyncio.coroutine
def async_added_to_hass(self) -> None:
async def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state."""
if self._initial_state is not None:
enable_automation = self._initial_state
_LOGGER.debug("Automation %s initial state %s from config "
"initial_state", self.entity_id, enable_automation)
else:
state = yield from async_get_last_state(self.hass, self.entity_id)
state = await async_get_last_state(self.hass, self.entity_id)
if state:
enable_automation = state.state == STATE_ON
self._last_triggered = state.attributes.get('last_triggered')
@ -298,54 +251,50 @@ class AutomationEntity(ToggleEntity):
# HomeAssistant is starting up
if self.hass.state == CoreState.not_running:
@asyncio.coroutine
def async_enable_automation(event):
async def async_enable_automation(event):
"""Start automation on startup."""
yield from self.async_enable()
await self.async_enable()
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, async_enable_automation)
# HomeAssistant is running
else:
yield from self.async_enable()
await self.async_enable()
@asyncio.coroutine
def async_turn_on(self, **kwargs) -> None:
async def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on and update the state."""
if self.is_on:
return
yield from self.async_enable()
await self.async_enable()
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
async def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
if not self.is_on:
return
self._async_detach_triggers()
self._async_detach_triggers = None
yield from self.async_update_ha_state()
await self.async_update_ha_state()
@asyncio.coroutine
def async_trigger(self, variables, skip_condition=False):
async def async_trigger(self, variables, skip_condition=False,
context=None):
"""Trigger automation.
This method is a coroutine.
"""
if skip_condition or self._cond_func(variables):
yield from self._async_action(self.entity_id, variables)
self.async_set_context(context)
await self._async_action(self.entity_id, variables, context)
self._last_triggered = utcnow()
yield from self.async_update_ha_state()
await self.async_update_ha_state()
@asyncio.coroutine
def async_will_remove_from_hass(self):
async def async_will_remove_from_hass(self):
"""Remove listeners when removing automation from HASS."""
yield from self.async_turn_off()
await self.async_turn_off()
@asyncio.coroutine
def async_enable(self):
async def async_enable(self):
"""Enable this automation entity.
This method is a coroutine.
@ -353,9 +302,9 @@ class AutomationEntity(ToggleEntity):
if self.is_on:
return
self._async_detach_triggers = yield from self._async_attach_triggers(
self._async_detach_triggers = await self._async_attach_triggers(
self.async_trigger)
yield from self.async_update_ha_state()
await self.async_update_ha_state()
@property
def device_state_attributes(self):
@ -368,8 +317,7 @@ class AutomationEntity(ToggleEntity):
}
@asyncio.coroutine
def _async_process_config(hass, config, component):
async def _async_process_config(hass, config, component):
"""Process config and add automations.
This method is a coroutine.
@ -411,20 +359,19 @@ def _async_process_config(hass, config, component):
entities.append(entity)
if entities:
yield from component.async_add_entities(entities)
await component.async_add_entities(entities)
def _async_get_action(hass, config, name):
"""Return an action based on a configuration."""
script_obj = script.Script(hass, config, name)
@asyncio.coroutine
def action(entity_id, variables):
async def action(entity_id, variables, context):
"""Execute an action."""
_LOGGER.info('Executing %s', name)
logbook.async_log_entry(
hass, name, 'has been triggered', DOMAIN, entity_id)
yield from script_obj.async_run(variables)
await script_obj.async_run(variables, context)
return action
@ -448,8 +395,7 @@ def _async_process_if(hass, config, p_config):
return if_action
@asyncio.coroutine
def _async_process_trigger(hass, config, trigger_configs, name, action):
async def _async_process_trigger(hass, config, trigger_configs, name, action):
"""Set up the triggers.
This method is a coroutine.
@ -457,13 +403,13 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
removes = []
for conf in trigger_configs:
platform = yield from async_prepare_setup_platform(
platform = await async_prepare_setup_platform(
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
if platform is None:
return None
remove = yield from platform.async_trigger(hass, conf, action)
remove = await platform.async_trigger(hass, conf, action)
if not remove:
_LOGGER.error("Error setting up trigger %s", name)

View File

@ -45,11 +45,11 @@ def async_trigger(hass, config, action):
# If event data doesn't match requested schema, skip event
return
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'event',
'event': event,
},
})
}, context=event.context))
return hass.bus.async_listen(event_type, handle_event)

View File

@ -32,12 +32,12 @@ def async_trigger(hass, config, action):
@callback
def hass_shutdown(event):
"""Execute when Home Assistant is shutting down."""
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'homeassistant',
'event': event,
},
})
}, context=event.context))
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
hass_shutdown)
@ -45,11 +45,11 @@ def async_trigger(hass, config, action):
# Automation are enabled while hass is starting up, fire right away
# Check state because a config reload shouldn't trigger it.
if hass.state == CoreState.starting:
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'homeassistant',
'event': event,
},
})
}))
return lambda: None

View File

@ -66,7 +66,7 @@ def async_trigger(hass, config, action):
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'numeric_state',
'entity_id': entity,
@ -75,7 +75,7 @@ def async_trigger(hass, config, action):
'from_state': from_s,
'to_state': to_s,
}
})
}, context=to_s.context))
matching = check_numeric_state(entity, from_s, to_s)

View File

@ -43,7 +43,7 @@ def async_trigger(hass, config, action):
@callback
def call_action():
"""Call action with right context."""
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'state',
'entity_id': entity,
@ -51,7 +51,7 @@ def async_trigger(hass, config, action):
'to_state': to_s,
'for': time_delta,
}
})
}, context=to_s.context))
# Ignore changes to state attributes if from/to is in use
if (not match_all and from_s is not None and to_s is not None and

View File

@ -32,13 +32,13 @@ def async_trigger(hass, config, action):
@callback
def template_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action."""
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'template',
'entity_id': entity_id,
'from_state': from_s,
'to_state': to_s,
},
})
}, context=to_s.context))
return async_track_template(hass, value_template, template_listener)

View File

@ -51,7 +51,7 @@ def async_trigger(hass, config, action):
# pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \
event == EVENT_LEAVE and from_match and not to_match:
hass.async_run_job(action, {
hass.async_run_job(action({
'trigger': {
'platform': 'zone',
'entity_id': entity,
@ -60,7 +60,7 @@ def async_trigger(hass, config, action):
'zone': zone_state,
'event': event,
},
})
}, context=to_s.context))
return async_track_state_change(hass, entity_id, zone_automation_listener,
MATCH_ALL, MATCH_ALL)

View File

@ -124,7 +124,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
if not check_control_messages:
result['check_control_messages'] = 'OK'
else:
result['check_control_messages'] = check_control_messages
cbs_list = []
for message in check_control_messages:
cbs_list.append(message['ccmDescriptionShort'])
result['check_control_messages'] = cbs_list
elif self._attribute == 'charging_status':
result['charging_status'] = vehicle_state.charging_status.value
# pylint: disable=protected-access

View File

@ -127,6 +127,7 @@ class DeconzBinarySensor(BinarySensorDevice):
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
@ -134,4 +135,5 @@ class DeconzBinarySensor(BinarySensorDevice):
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
'via_hub': (DECONZ_DOMAIN, bridgeid),
}

View File

@ -27,17 +27,20 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector)
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector,
AsyncWaterSensor, AsyncRotaryHandleSensor)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
if isinstance(device, AsyncShutterContact):
if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)):
devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, AsyncMotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device))
elif isinstance(device, AsyncSmokeDetector):
devices.append(HomematicipSmokeDetector(home, device))
elif isinstance(device, AsyncWaterSensor):
devices.append(HomematicipWaterDetector(home, device))
if devices:
async_add_entities(devices)
@ -91,3 +94,17 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
def is_on(self):
"""Return true if smoke is detected."""
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF
class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud water detector."""
@property
def device_class(self):
"""Return the class of this sensor."""
return 'moisture'
@property
def is_on(self):
"""Return true if moisture or waterlevel is detected."""
return self._device.moistureDetected or self._device.waterlevelDetected

View File

@ -11,16 +11,20 @@ from typing import Optional
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import mqtt
from homeassistant.components import mqtt, binary_sensor
from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS)
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability, MqttDiscoveryUpdate)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__)
@ -44,13 +48,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the MQTT binary sensor."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None):
"""Set up MQTT binary sensor through configuration.yaml."""
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT binary sensor dynamically through MQTT discovery."""
async def async_discover(discovery_payload):
"""Discover and add a MQTT binary sensor."""
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(hass, config, async_add_entities,
discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(binary_sensor.DOMAIN, 'mqtt'),
async_discover)
async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None):
"""Set up the MQTT binary sensor."""
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = hass
@ -68,19 +87,22 @@ def async_setup_platform(hass, config, async_add_entities,
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
value_template,
config.get(CONF_UNIQUE_ID),
discovery_hash,
)])
class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT."""
def __init__(self, name, state_topic, availability_topic, device_class,
qos, force_update, payload_on, payload_off, payload_available,
payload_not_available, value_template,
unique_id: Optional[str]):
unique_id: Optional[str], discovery_hash):
"""Initialize the MQTT binary sensor."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
self._name = name
self._state = None
self._state_topic = state_topic
@ -91,11 +113,13 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
self._force_update = force_update
self._template = value_template
self._unique_id = unique_id
self._discovery_hash = discovery_hash
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe mqtt events."""
yield from super().async_added_to_hass()
yield from MqttAvailability.async_added_to_hass(self)
yield from MqttDiscoveryUpdate.async_added_to_hass(self)
@callback
def state_message_received(topic, payload, qos):

View File

@ -147,6 +147,11 @@ class NestActivityZoneSensor(NestBinarySensor):
self.zone = zone
self._name = "{} {} activity".format(self._name, self.zone.name)
@property
def unique_id(self):
"""Return unique id based on camera serial and zone id."""
return "{}-{}".format(self.device.serial, self.zone.zone_id)
@property
def device_class(self):
"""Return the device class of the binary sensor."""

View File

@ -7,12 +7,11 @@ https://home-assistant.io/components/binary_sensor.openuv/
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.openuv import (
BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE,
TYPE_PROTECTION_WINDOW, OpenUvEntity)
BINARY_SENSORS, DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN,
TOPIC_UPDATE, TYPE_PROTECTION_WINDOW, OpenUvEntity)
from homeassistant.util.dt import as_local, parse_datetime, utcnow
DEPENDENCIES = ['openuv']
@ -26,17 +25,20 @@ ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the OpenUV binary sensor platform."""
if discovery_info is None:
return
"""Set up an OpenUV sensor based on existing config."""
pass
openuv = hass.data[DOMAIN]
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up an OpenUV sensor based on a config entry."""
openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id]
binary_sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
for sensor_type in openuv.binary_sensor_conditions:
name, icon = BINARY_SENSORS[sensor_type]
binary_sensors.append(
OpenUvBinarySensor(openuv, sensor_type, name, icon))
OpenUvBinarySensor(
openuv, sensor_type, name, icon, entry.entry_id))
async_add_entities(binary_sensors, True)
@ -44,14 +46,16 @@ async def async_setup_platform(
class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
"""Define a binary sensor for OpenUV."""
def __init__(self, openuv, sensor_type, name, icon):
def __init__(self, openuv, sensor_type, name, icon, entry_id):
"""Initialize the sensor."""
super().__init__(openuv)
self._entry_id = entry_id
self._icon = icon
self._latitude = openuv.client.latitude
self._longitude = openuv.client.longitude
self._name = name
self._dispatch_remove = None
self._sensor_type = sensor_type
self._state = None
@ -83,8 +87,9 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self._dispatch_remove = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, self._update_data)
self.async_on_remove(self._dispatch_remove)
async def async_update(self):
"""Update the state."""

View File

@ -92,6 +92,11 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
"""Return the name of this sensor including the controller name."""
return "{} online".format(self._controller.name)
@property
def unique_id(self) -> str:
"""Return a unique id for this entity."""
return "{}-online".format(self._controller.controller_id)
@property
def device_class(self) -> str:
"""Return the class of this device, from component DEVICE_CLASSES."""

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
_LOGGER = logging.getLogger(__name__)
@ -66,13 +67,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update()
if rest.data is None:
_LOGGER.error("Unable to fetch REST data from %s", resource)
return False
raise PlatformNotReady
# No need to update the sensor now because it will determine its state
# based in the rest resource that has just been retrieved.
add_entities([RestBinarySensor(
hass, rest, name, device_class, value_template)], True)
hass, rest, name, device_class, value_template)])
class RestBinarySensor(BinarySensorDevice):

View File

@ -44,14 +44,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
ring = hass.data[DATA_RING]
sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
for device in ring.doorbells:
for device in ring.doorbells: # ring.doorbells is doing I/O
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
if 'doorbell' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass,
device,
sensor_type))
for device in ring.stickup_cams:
for device in ring.stickup_cams: # ring.stickup_cams is doing I/O
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass,
device,

View File

@ -4,87 +4,66 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.spc/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.spc import ATTR_DISCOVER_DEVICES, DATA_REGISTRY
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
from homeassistant.components.spc import (
ATTR_DISCOVER_DEVICES, SIGNAL_UPDATE_SENSOR)
_LOGGER = logging.getLogger(__name__)
SPC_TYPE_TO_DEVICE_CLASS = {
'0': 'motion',
'1': 'opening',
'3': 'smoke',
}
SPC_INPUT_TO_SENSOR_STATE = {
'0': STATE_OFF,
'1': STATE_ON,
}
def _get_device_class(zone_type):
from pyspcwebgw.const import ZoneType
return {
ZoneType.ALARM: 'motion',
ZoneType.ENTRY_EXIT: 'opening',
ZoneType.FIRE: 'smoke',
}.get(zone_type)
def _get_device_class(spc_type):
"""Get the device class."""
return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None)
def _get_sensor_state(spc_input):
"""Get the sensor state."""
return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE)
def _create_sensor(hass, zone):
"""Create a SPC sensor."""
return SpcBinarySensor(
zone_id=zone['id'], name=zone['zone_name'],
state=_get_sensor_state(zone['input']),
device_class=_get_device_class(zone['type']),
spc_registry=hass.data[DATA_REGISTRY])
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the SPC binary sensor."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None):
return
async_add_entities(
_create_sensor(hass, zone)
for zone in discovery_info[ATTR_DISCOVER_DEVICES]
if _get_device_class(zone['type']))
async_add_entities(SpcBinarySensor(zone)
for zone in discovery_info[ATTR_DISCOVER_DEVICES]
if _get_device_class(zone.type))
class SpcBinarySensor(BinarySensorDevice):
"""Representation of a sensor based on a SPC zone."""
def __init__(self, zone_id, name, state, device_class, spc_registry):
def __init__(self, zone):
"""Initialize the sensor device."""
self._zone_id = zone_id
self._name = name
self._state = state
self._device_class = device_class
self._zone = zone
spc_registry.register_sensor_device(zone_id, self)
async def async_added_to_hass(self):
"""Call for adding new entities."""
async_dispatcher_connect(self.hass,
SIGNAL_UPDATE_SENSOR.format(self._zone.id),
self._update_callback)
@asyncio.coroutine
def async_update_from_spc(self, state, extra):
"""Update the state of the device."""
self._state = state
yield from self.async_update_ha_state()
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def name(self):
"""Return the name of the device."""
return self._name
return self._zone.name
@property
def is_on(self):
"""Whether the device is switched on."""
return self._state == STATE_ON
from pyspcwebgw.const import ZoneInput
return self._zone.input == ZoneInput.OPEN
@property
def hidden(self) -> bool:
@ -100,4 +79,4 @@ class SpcBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""Return the device class."""
return self._device_class
return _get_device_class(self._zone.type)

View File

@ -14,9 +14,6 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.wirelesstag import (
DOMAIN as WIRELESSTAG_DOMAIN,
WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
WIRELESSTAG_TYPE_ALSPRO,
WIRELESSTAG_TYPE_WEMO_DEVICE,
SIGNAL_BINARY_EVENT_UPDATE,
WirelessTagBaseSensor)
from homeassistant.const import (
@ -30,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
# On means in range, Off means out of range
SENSOR_PRESENCE = 'presence'
# On means motion detected, Off means cear
# On means motion detected, Off means clear
SENSOR_MOTION = 'motion'
# On means open, Off means closed
@ -55,49 +52,21 @@ SENSOR_LIGHT = 'light'
SENSOR_MOISTURE = 'moisture'
# On means tag battery is low, Off means normal
SENSOR_BATTERY = 'low_battery'
SENSOR_BATTERY = 'battery'
# Sensor types: Name, device_class, push notification type representing 'on',
# attr to check
SENSOR_TYPES = {
SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', {
"on": "oor",
"off": "back_in_range"
}, 2],
SENSOR_MOTION: ['Motion', 'motion', 'is_moved', {
"on": "motion_detected",
}, 5],
SENSOR_DOOR: ['Door', 'door', 'is_door_open', {
"on": "door_opened",
"off": "door_closed"
}, 5],
SENSOR_COLD: ['Cold', 'cold', 'is_cold', {
"on": "temp_toolow",
"off": "temp_normal"
}, 4],
SENSOR_HEAT: ['Heat', 'heat', 'is_heat', {
"on": "temp_toohigh",
"off": "temp_normal"
}, 4],
SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', {
"on": "too_dry",
"off": "cap_normal"
}, 2],
SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', {
"on": "too_humid",
"off": "cap_normal"
}, 2],
SENSOR_LIGHT: ['Light', 'light', 'is_light_on', {
"on": "too_bright",
"off": "light_normal"
}, 1],
SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', {
"on": "water_detected",
"off": "water_dried",
}, 1],
SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', {
"on": "low_battery"
}, 3]
SENSOR_PRESENCE: 'Presence',
SENSOR_MOTION: 'Motion',
SENSOR_DOOR: 'Door',
SENSOR_COLD: 'Cold',
SENSOR_HEAT: 'Heat',
SENSOR_DRY: 'Too dry',
SENSOR_WET: 'Too wet',
SENSOR_LIGHT: 'Light',
SENSOR_MOISTURE: 'Leak',
SENSOR_BATTERY: 'Low Battery'
}
@ -114,7 +83,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sensors = []
tags = platform.tags
for tag in tags.values():
allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag)
allowed_sensor_types = tag.supported_binary_events_types
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type in allowed_sensor_types:
sensors.append(WirelessTagBinarySensor(platform, tag,
@ -127,59 +96,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
"""A binary sensor implementation for WirelessTags."""
@classmethod
def allowed_sensors(cls, tag):
"""Return list of allowed sensor types for specific tag type."""
sensors_map = {
# 13-bit tag - allows everything but not light and moisture
WIRELESSTAG_TYPE_13BIT: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_MOTION, SENSOR_DOOR,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_DRY, SENSOR_WET],
# Moister/water sensor - temperature and moisture only
WIRELESSTAG_TYPE_WATER: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_MOISTURE],
# ALS Pro: allows everything, but not moisture
WIRELESSTAG_TYPE_ALSPRO: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_MOTION, SENSOR_DOOR,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_DRY, SENSOR_WET,
SENSOR_LIGHT],
# Wemo are power switches.
WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE]
}
# allow everything if tag type is unknown
# (i just dont have full catalog of them :))
tag_type = tag.tag_type
fullset = SENSOR_TYPES.keys()
return sensors_map[tag_type] if tag_type in sensors_map else fullset
def __init__(self, api, tag, sensor_type):
"""Initialize a binary sensor for a Wireless Sensor Tags."""
super().__init__(api, tag)
self._sensor_type = sensor_type
self._name = '{0} {1}'.format(self._tag.name,
SENSOR_TYPES[self._sensor_type][0])
self._device_class = SENSOR_TYPES[self._sensor_type][1]
self._tag_attr = SENSOR_TYPES[self._sensor_type][2]
self.binary_spec = SENSOR_TYPES[self._sensor_type][3]
self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4]
self.event.human_readable_name)
async def async_added_to_hass(self):
"""Register callbacks."""
tag_id = self.tag_id
event_type = self.device_class
mac = self.tag_manager_mac
async_dispatcher_connect(
self.hass,
SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type),
SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac),
self._on_binary_event_callback)
@property
@ -190,7 +121,12 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class
return self._sensor_type
@property
def event(self):
"""Binary event of tag."""
return self._tag.event[self._sensor_type]
@property
def principal_value(self):
@ -198,9 +134,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
Subclasses need override based on type of sensor.
"""
return (
STATE_ON if getattr(self._tag, self._tag_attr, False)
else STATE_OFF)
return STATE_ON if self.event.is_state_on else STATE_OFF
def updated_state_value(self):
"""Use raw princial value."""
@ -208,7 +142,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
@callback
def _on_binary_event_callback(self, event):
"""Update state from arrive push notification."""
"""Update state from arrived push notification."""
# state should be 'on' or 'off'
self._state = event.data.get('state')
self.async_schedule_update_ha_state()

View File

@ -15,35 +15,38 @@ from homeassistant.const import CONF_NAME, WEEKDAYS
from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.7']
REQUIREMENTS = ['holidays==0.9.6']
_LOGGER = logging.getLogger(__name__)
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland',
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
'UnitedStates', 'US', 'Wales']
ALL_COUNTRIES = [
'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY'
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ',
'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ',
'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK',
'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH',
'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
]
ALLOWED_DAYS = WEEKDAYS + ['holiday']
CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
CONF_EXCLUDES = 'excludes'
CONF_OFFSET = 'days_offset'
# By default, Monday - Friday are workdays
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
CONF_EXCLUDES = 'excludes'
# By default, public holidays, Saturdays and Sundays are excluded from workdays
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
DEFAULT_NAME = 'Workday Sensor'
ALLOWED_DAYS = WEEKDAYS + ['holiday']
CONF_OFFSET = 'days_offset'
DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -86,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else:
_LOGGER.error("There is no province/state %s in country %s",
province, country)
return False
return
_LOGGER.debug("Found the following holidays for your configuration:")
for date, name in sorted(obj_holidays.items()):

View File

@ -65,28 +65,25 @@ async def _async_setup_iaszone(hass, config, async_add_entities,
async def _async_setup_remote(hass, config, async_add_entities,
discovery_info):
async def safe(coro):
"""Run coro, catching ZigBee delivery errors, and ignoring them."""
import zigpy.exceptions
try:
await coro
except zigpy.exceptions.DeliveryError as exc:
_LOGGER.warning("Ignoring error during setup: %s", exc)
remote = Remote(**discovery_info)
if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 0, 600, 1))
await zha.configure_reporting(
remote.entity_id, cluster, 0, min_report=0, max_report=600,
reportable_change=1
)
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
await zha.configure_reporting(
remote.entity_id, cluster, 0, min_report=1, max_report=600,
reportable_change=1
)
sensor = Switch(**discovery_info)
async_add_entities([sensor], update_before_add=True)
async_add_entities([remote], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice):
@ -131,17 +128,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
async def async_update(self):
"""Retrieve latest state."""
from bellows.types.basic import uint16_t
from zigpy.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status'],
allow_cache=False)
allow_cache=False,
only_cache=(not self._initialized))
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
class Switch(zha.Entity, BinarySensorDevice):
class Remote(zha.Entity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN

View File

@ -9,8 +9,8 @@ import datetime
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time
from homeassistant.components import zwave
from homeassistant.components.zwave import workaround
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
async_setup_platform, workaround)
from homeassistant.components.binary_sensor import (
DOMAIN,
BinarySensorDevice)

View File

@ -14,7 +14,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['bimmer_connected==0.5.1']
REQUIREMENTS = ['bimmer_connected==0.5.3']
_LOGGER = logging.getLogger(__name__)

View File

@ -83,65 +83,71 @@ class Image:
content = attr.ib(type=bytes)
@bind_hass
def turn_off(hass, entity_id=None):
"""Turn off camera."""
hass.add_job(async_turn_off, hass, entity_id)
@bind_hass
async def async_turn_off(hass, entity_id=None):
"""Turn off camera."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)
@bind_hass
def turn_on(hass, entity_id=None):
"""Turn on camera."""
hass.add_job(async_turn_on, hass, entity_id)
@bind_hass
async def async_turn_on(hass, entity_id=None):
"""Turn on camera, and set operation mode."""
data = {}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)
@bind_hass
def enable_motion_detection(hass, entity_id=None):
"""Enable Motion Detection."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_ENABLE_MOTION, data))
@bind_hass
def disable_motion_detection(hass, entity_id=None):
"""Disable Motion Detection."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_DISABLE_MOTION, data))
@bind_hass
@callback
def async_snapshot(hass, filename, entity_id=None):
"""Make a snapshot from a camera."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
data[ATTR_FILENAME] = filename
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_SNAPSHOT, data))
@bind_hass
async def async_get_image(hass, entity_id, timeout=10):
"""Fetch an image from a camera entity."""
camera = _get_camera_from_entity_id(hass, entity_id)
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop):
image = await camera.async_camera_image()
if image:
return Image(camera.content_type, image)
raise HomeAssistantError('Unable to get image')
@bind_hass
async def async_get_mjpeg_stream(hass, request, entity_id):
"""Fetch an mjpeg stream from a camera entity."""
camera = _get_camera_from_entity_id(hass, entity_id)
return await camera.handle_async_mjpeg_stream(request)
async def async_get_still_stream(request, image_cb, content_type, interval):
"""Generate an HTTP MJPEG stream from camera images.
This method must be run in the event loop.
"""
response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary')
await response.prepare(request)
async def write_to_mjpeg_stream(img_bytes):
"""Write image to stream."""
await response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
last_image = None
while True:
img_bytes = await image_cb()
if not img_bytes:
break
if img_bytes != last_image:
await write_to_mjpeg_stream(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes
await asyncio.sleep(interval)
return response
def _get_camera_from_entity_id(hass, entity_id):
"""Get camera component from entity_id."""
component = hass.data.get(DOMAIN)
if component is None:
@ -155,14 +161,7 @@ async def async_get_image(hass, entity_id, timeout=10):
if not camera.is_on:
raise HomeAssistantError('Camera is off')
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop):
image = await camera.async_camera_image()
if image:
return Image(camera.content_type, image)
raise HomeAssistantError('Unable to get image')
return camera
async def async_setup(hass, config):
@ -290,39 +289,8 @@ class Camera(Entity):
This method must be run in the event loop.
"""
response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary')
await response.prepare(request)
async def write_to_mjpeg_stream(img_bytes):
"""Write image to stream."""
await response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
self.content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
last_image = None
while True:
img_bytes = await self.async_camera_image()
if not img_bytes:
break
if img_bytes and img_bytes != last_image:
await write_to_mjpeg_stream(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes
await asyncio.sleep(interval)
return response
return await async_get_still_stream(request, self.async_camera_image,
self.content_type, interval)
async def handle_async_mjpeg_stream(self, request):
"""Serve an HTTP MJPEG stream from the camera.

View File

@ -50,7 +50,7 @@ class AxisCamera(MjpegCamera):
def __init__(self, hass, config, port):
"""Initialize Axis Communications camera component."""
super().__init__(hass, config)
super().__init__(config)
self.port = port
dispatcher_connect(
hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip)

View File

@ -0,0 +1,210 @@
"""
This component provides support to the Logi Circle camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.logi_circle/
"""
import logging
import asyncio
from datetime import timedelta
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.components.logi_circle import (
DOMAIN as LOGI_CIRCLE_DOMAIN, CONF_ATTRIBUTION)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF,
ATTR_ENTITY_ID, ATTR_FILENAME, DOMAIN)
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL,
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF)
DEPENDENCIES = ['logi_circle']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
SERVICE_SET_CONFIG = 'logi_circle_set_config'
SERVICE_LIVESTREAM_SNAPSHOT = 'logi_circle_livestream_snapshot'
SERVICE_LIVESTREAM_RECORD = 'logi_circle_livestream_record'
DATA_KEY = 'camera.logi_circle'
BATTERY_SAVING_MODE_KEY = 'BATTERY_SAVING'
PRIVACY_MODE_KEY = 'PRIVACY_MODE'
LED_MODE_KEY = 'LED'
ATTR_MODE = 'mode'
ATTR_VALUE = 'value'
ATTR_DURATION = 'duration'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
})
LOGI_CIRCLE_SERVICE_SET_CONFIG = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_MODE): vol.In([BATTERY_SAVING_MODE_KEY, LED_MODE_KEY,
PRIVACY_MODE_KEY]),
vol.Required(ATTR_VALUE): cv.boolean
})
LOGI_CIRCLE_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FILENAME): cv.template
})
LOGI_CIRCLE_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FILENAME): cv.template,
vol.Required(ATTR_DURATION): cv.positive_int
})
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up a Logi Circle Camera."""
devices = hass.data[LOGI_CIRCLE_DOMAIN]
cameras = []
for device in devices:
cameras.append(LogiCam(device, config))
async_add_entities(cameras, True)
async def service_handler(service):
"""Dispatch service calls to target entities."""
params = {key: value for key, value in service.data.items()
if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
target_devices = [dev for dev in cameras
if dev.entity_id in entity_ids]
else:
target_devices = cameras
for target_device in target_devices:
if service.service == SERVICE_SET_CONFIG:
await target_device.set_config(**params)
if service.service == SERVICE_LIVESTREAM_SNAPSHOT:
await target_device.livestream_snapshot(**params)
if service.service == SERVICE_LIVESTREAM_RECORD:
await target_device.download_livestream(**params)
hass.services.async_register(
DOMAIN, SERVICE_SET_CONFIG, service_handler,
schema=LOGI_CIRCLE_SERVICE_SET_CONFIG)
hass.services.async_register(
DOMAIN, SERVICE_LIVESTREAM_SNAPSHOT, service_handler,
schema=LOGI_CIRCLE_SERVICE_SNAPSHOT)
hass.services.async_register(
DOMAIN, SERVICE_LIVESTREAM_RECORD, service_handler,
schema=LOGI_CIRCLE_SERVICE_RECORD)
class LogiCam(Camera):
"""An implementation of a Logi Circle camera."""
def __init__(self, camera, device_info):
"""Initialize Logi Circle camera."""
super().__init__()
self._camera = camera
self._name = self._camera.name
self._id = self._camera.mac_address
self._has_battery = self._camera.supports_feature('battery_level')
@property
def unique_id(self):
"""Return a unique ID."""
return self._id
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def supported_features(self):
"""Logi Circle camera's support turning on and off ("soft" switch)."""
return SUPPORT_ON_OFF
@property
def device_state_attributes(self):
"""Return the state attributes."""
state = {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'battery_saving_mode': (
STATE_ON if self._camera.battery_saving else STATE_OFF),
'ip_address': self._camera.ip_address,
'microphone_gain': self._camera.microphone_gain
}
# Add battery attributes if camera is battery-powered
if self._has_battery:
state[ATTR_BATTERY_CHARGING] = self._camera.is_charging
state[ATTR_BATTERY_LEVEL] = self._camera.battery_level
return state
async def async_camera_image(self):
"""Return a still image from the camera."""
return await self._camera.get_snapshot_image()
async def async_turn_off(self):
"""Disable streaming mode for this camera."""
await self._camera.set_streaming_mode(False)
async def async_turn_on(self):
"""Enable streaming mode for this camera."""
await self._camera.set_streaming_mode(True)
@property
def should_poll(self):
"""Update the image periodically."""
return True
async def set_config(self, mode, value):
"""Set an configuration property for the target camera."""
if mode == LED_MODE_KEY:
await self._camera.set_led(value)
if mode == PRIVACY_MODE_KEY:
await self._camera.set_privacy_mode(value)
if mode == BATTERY_SAVING_MODE_KEY:
await self._camera.set_battery_saving_mode(value)
async def download_livestream(self, filename, duration):
"""Download a recording from the camera's livestream."""
# Render filename from template.
filename.hass = self.hass
stream_file = filename.async_render(
variables={ATTR_ENTITY_ID: self.entity_id})
# Respect configured path whitelist.
if not self.hass.config.is_allowed_path(stream_file):
_LOGGER.error(
"Can't write %s, no access to path!", stream_file)
return
asyncio.shield(self._camera.record_livestream(
stream_file, timedelta(seconds=duration)), loop=self.hass.loop)
async def livestream_snapshot(self, filename):
"""Download a still frame from the camera's livestream."""
# Render filename from template.
filename.hass = self.hass
snapshot_file = filename.async_render(
variables={ATTR_ENTITY_ID: self.entity_id})
# Respect configured path whitelist.
if not self.hass.config.is_allowed_path(snapshot_file):
_LOGGER.error(
"Can't write %s, no access to path!", snapshot_file)
return
asyncio.shield(self._camera.get_livestream_image(
snapshot_file), loop=self.hass.loop)
async def async_update(self):
"""Update camera entity and refresh attributes."""
await self._camera.update()

View File

@ -47,7 +47,7 @@ def async_setup_platform(hass, config, async_add_entities,
"""Set up a MJPEG IP Camera."""
if discovery_info:
config = PLATFORM_SCHEMA(discovery_info)
async_add_entities([MjpegCamera(hass, config)])
async_add_entities([MjpegCamera(config)])
def extract_image_from_mjpeg(stream):
@ -65,7 +65,7 @@ def extract_image_from_mjpeg(stream):
class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL."""
def __init__(self, hass, device_info):
def __init__(self, device_info):
"""Initialize a MJPEG camera."""
super().__init__()
self._name = device_info.get(CONF_NAME)

View File

@ -10,34 +10,53 @@ import logging
import voluptuous as vol
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.core import callback
from homeassistant.components import mqtt
from homeassistant.const import CONF_NAME
from homeassistant.components import mqtt, camera
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic'
CONF_UNIQUE_ID = 'unique_id'
DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the MQTT Camera."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None):
"""Set up MQTT camera through configuration.yaml."""
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT camera dynamically through MQTT discovery."""
async def async_discover(discovery_payload):
"""Discover and add a MQTT camera."""
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(hass, config, async_add_entities)
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(camera.DOMAIN, 'mqtt'),
async_discover)
async def _async_setup_entity(hass, config, async_add_entities):
"""Set up the MQTT Camera."""
async_add_entities([MqttCamera(
config.get(CONF_NAME),
config.get(CONF_UNIQUE_ID),
config.get(CONF_TOPIC)
)])
@ -45,11 +64,12 @@ def async_setup_platform(hass, config, async_add_entities,
class MqttCamera(Camera):
"""representation of a MQTT camera."""
def __init__(self, name, topic):
def __init__(self, name, unique_id, topic):
"""Initialize the MQTT Camera."""
super().__init__()
self._name = name
self._unique_id = unique_id
self._topic = topic
self._qos = 0
self._last_image = None
@ -64,6 +84,11 @@ class MqttCamera(Camera):
"""Return the name of this camera."""
return self._name
@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe MQTT events."""

View File

@ -62,6 +62,23 @@ class NestCamera(Camera):
"""Return the name of the nest, if any."""
return self._name
@property
def unique_id(self):
"""Return the serial number."""
return self.device.device_id
@property
def device_info(self):
"""Return information about the device."""
return {
'identifiers': {
(nest.DOMAIN, self.device.device_id)
},
'name': self.device.name_long,
'manufacturer': 'Nest Labs',
'model': "Camera",
}
@property
def should_poll(self):
"""Nest camera should poll periodically."""

View File

@ -7,17 +7,15 @@ https://www.home-assistant.io/components/camera.proxy/
import asyncio
import logging
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_web, async_get_clientsession)
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
from . import async_get_still_stream
REQUIREMENTS = ['pillow==5.2.0']
@ -158,22 +156,14 @@ class ProxyCamera(Camera):
return self._last_image
self._last_image_time = now
url = "{}/api/camera_proxy/{}".format(
self.hass.config.api.base_url, self._proxied_camera)
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop):
response = await websession.get(url, headers=self._headers)
image = await response.read()
except asyncio.TimeoutError:
_LOGGER.error("Timeout getting camera image")
return self._last_image
except aiohttp.ClientError as err:
_LOGGER.error("Error getting new camera image: %s", err)
image = await self.hass.components.camera.async_get_image(
self._proxied_camera)
if not image:
_LOGGER.error("Error getting original camera image")
return self._last_image
image = await self.hass.async_add_job(
_resize_image, image, self._image_opts)
_resize_image, image.content, self._image_opts)
if self._cache_images:
self._last_image = image
@ -181,56 +171,28 @@ class ProxyCamera(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images."""
websession = async_get_clientsession(self.hass)
url = "{}/api/camera_proxy_stream/{}".format(
self.hass.config.api.base_url, self._proxied_camera)
stream_coro = websession.get(url, headers=self._headers)
if not self._stream_opts:
return await async_aiohttp_proxy_web(
self.hass, request, stream_coro)
return await self.hass.components.camera.async_get_mjpeg_stream(
request, self._proxied_camera)
response = aiohttp.web.StreamResponse()
response.content_type = (
'multipart/x-mixed-replace; boundary=--frameboundary')
await response.prepare(request)
async def write(img_bytes):
"""Write image to stream."""
await response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
self.content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
with async_timeout.timeout(10, loop=self.hass.loop):
req = await stream_coro
try:
# This would be nicer as an async generator
# But that would only be supported for python >=3.6
data = b''
stream = req.content
while True:
chunk = await stream.read(102400)
if not chunk:
break
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
image = data[jpg_start:jpg_end + 2]
image = await self.hass.async_add_job(
_resize_image, image, self._stream_opts)
await write(image)
data = data[jpg_end + 2:]
finally:
req.close()
return response
return await async_get_still_stream(
request, self._async_stream_image,
self.content_type, self.frame_interval)
@property
def name(self):
"""Return the name of this camera."""
return self._name
async def _async_stream_image(self):
"""Return a still image response from the camera."""
try:
image = await self.hass.components.camera.async_get_image(
self._proxied_camera)
if not image:
return None
except HomeAssistantError:
raise asyncio.CancelledError
return await self.hass.async_add_job(
_resize_image, image.content, self._stream_opts)

View File

@ -13,8 +13,10 @@ import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING
from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST
from homeassistant.components.http.view import KEY_AUTHENTICATED,\
HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\
HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util
@ -25,11 +27,13 @@ DEPENDENCIES = ['http']
CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field'
CONF_TOKEN = 'token'
DEFAULT_NAME = "Push Camera"
ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip'
ATTR_TOKEN = 'token'
PUSH_CAMERA_DATA = 'push_camera'
@ -39,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string,
vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)),
})
@ -50,7 +55,8 @@ async def async_setup_platform(hass, config, async_add_entities,
cameras = [PushCamera(config[CONF_NAME],
config[CONF_BUFFER_SIZE],
config[CONF_TIMEOUT])]
config[CONF_TIMEOUT],
config.get(CONF_TOKEN))]
hass.http.register_view(CameraPushReceiver(hass,
config[CONF_IMAGE_FIELD]))
@ -63,6 +69,7 @@ class CameraPushReceiver(HomeAssistantView):
url = "/api/camera_push/{entity_id}"
name = 'api:camera_push:camera_entity'
requires_auth = False
def __init__(self, hass, image_field):
"""Initialize CameraPushReceiver with camera entity."""
@ -75,8 +82,21 @@ class CameraPushReceiver(HomeAssistantView):
if _camera is None:
_LOGGER.error("Unknown %s", entity_id)
status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\
else HTTP_UNAUTHORIZED
return self.json_message('Unknown {}'.format(entity_id),
HTTP_BAD_REQUEST)
status)
# Supports HA authentication and token based
# when token has been configured
authenticated = (request[KEY_AUTHENTICATED] or
(_camera.token is not None and
request.query.get('token') == _camera.token))
if not authenticated:
return self.json_message(
'Invalid authorization credentials for {}'.format(entity_id),
HTTP_UNAUTHORIZED)
try:
data = await request.post()
@ -95,7 +115,7 @@ class CameraPushReceiver(HomeAssistantView):
class PushCamera(Camera):
"""The representation of a Push camera."""
def __init__(self, name, buffer_size, timeout):
def __init__(self, name, buffer_size, timeout, token):
"""Initialize push camera component."""
super().__init__()
self._name = name
@ -106,6 +126,7 @@ class PushCamera(Camera):
self._timeout = timeout
self.queue = deque([], buffer_size)
self._current_image = None
self.token = token
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
@ -168,5 +189,6 @@ class PushCamera(Camera):
name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename),
(ATTR_TOKEN, self.token),
) if value is not None
}

View File

@ -39,9 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Ring Door Bell and StickUp Camera."""
ring = hass.data[DATA_RING]
@ -67,14 +65,14 @@ def async_setup_platform(hass, config, async_add_entities,
''' following cameras: {}.'''.format(cameras)
_LOGGER.error(err_msg)
hass.components.persistent_notification.async_create(
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(err_msg),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
async_add_entities(cams, True)
add_entities(cams, True)
return True

View File

@ -63,3 +63,39 @@ onvif_ptz:
zoom:
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
example: "ZOOM_IN"
logi_circle_set_config:
description: Set a configuration property.
fields:
entity_id:
description: Name(s) of entities to apply the operation mode to.
example: "camera.living_room_camera"
mode:
description: "Operation mode. Allowed values: BATTERY_SAVING, LED, PRIVACY_MODE."
example: "PRIVACY_MODE"
value:
description: "Operation value. Allowed values: true, false"
example: true
logi_circle_livestream_snapshot:
description: Take a snapshot from the camera's livestream. Will wake the camera from sleep if required.
fields:
entity_id:
description: Name(s) of entities to create snapshots from.
example: "camera.living_room_camera"
filename:
description: Template of a Filename. Variable is entity_id.
example: "/tmp/snapshot_{{ entity_id }}.jpg"
logi_circle_livestream_record:
description: Take a video recording from the camera's livestream.
fields:
entity_id:
description: Name(s) of entities to create recordings from.
example: "camera.living_room_camera"
filename:
description: Template of a Filename. Variable is entity_id.
example: "/tmp/snapshot_{{ entity_id }}.mp4"
duration:
description: Recording duration in seconds.
example: 60

View File

@ -4,91 +4,47 @@ Support for ZoneMinder camera streaming.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.zoneminder/
"""
import asyncio
import logging
from urllib.parse import urljoin, urlencode
from homeassistant.const import CONF_NAME
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
from homeassistant.components import zoneminder
from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zoneminder']
DOMAIN = 'zoneminder'
# From ZoneMinder's web/includes/config.php.in
ZM_STATE_ALARM = "2"
def _get_image_url(hass, monitor, mode):
zm_data = hass.data[DOMAIN]
query = urlencode({
'mode': mode,
'buffer': monitor['StreamReplayBuffer'],
'monitor': monitor['Id'],
})
url = '{zms_url}?{query}'.format(
zms_url=urljoin(zm_data['server_origin'], zm_data['path_zms']),
query=query,
)
_LOGGER.debug('Monitor %s %s URL (without auth): %s',
monitor['Id'], mode, url)
if not zm_data['username']:
return url
url += '&user={:s}'.format(zm_data['username'])
if not zm_data['password']:
return url
return url + '&pass={:s}'.format(zm_data['password'])
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder cameras."""
cameras = []
monitors = zoneminder.get_state('api/monitors.json')
zm_client = hass.data[ZONEMINDER_DOMAIN]
monitors = zm_client.get_monitors()
if not monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
return
for i in monitors['monitors']:
monitor = i['Monitor']
if monitor['Function'] == 'None':
_LOGGER.info("Skipping camera %s", monitor['Id'])
continue
_LOGGER.info("Initializing camera %s", monitor['Id'])
device_info = {
CONF_NAME: monitor['Name'],
CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
}
cameras.append(ZoneMinderCamera(hass, device_info, monitor))
if not cameras:
_LOGGER.warning("No active cameras found")
return
async_add_entities(cameras)
cameras = []
for monitor in monitors:
_LOGGER.info("Initializing camera %s", monitor.id)
cameras.append(ZoneMinderCamera(monitor))
add_entities(cameras)
class ZoneMinderCamera(MjpegCamera):
"""Representation of a ZoneMinder Monitor Stream."""
def __init__(self, hass, device_info, monitor):
def __init__(self, monitor):
"""Initialize as a subclass of MjpegCamera."""
super().__init__(hass, device_info)
self._monitor_id = int(monitor['Id'])
device_info = {
CONF_NAME: monitor.name,
CONF_MJPEG_URL: monitor.mjpeg_image_url,
CONF_STILL_IMAGE_URL: monitor.still_image_url
}
super().__init__(device_info)
self._is_recording = None
self._monitor = monitor
@property
def should_poll(self):
@ -97,17 +53,8 @@ class ZoneMinderCamera(MjpegCamera):
def update(self):
"""Update our recording state from the ZM API."""
_LOGGER.debug("Updating camera state for monitor %i", self._monitor_id)
status_response = zoneminder.get_state(
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
)
if not status_response:
_LOGGER.warning("Could not get status for monitor %i",
self._monitor_id)
return
self._is_recording = status_response.get('status') == ZM_STATE_ALARM
_LOGGER.debug("Updating camera state for monitor %i", self._monitor.id)
self._is_recording = self._monitor.is_recording
@property
def is_recording(self):

View File

@ -2,7 +2,7 @@
"config": {
"abort": {
"no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.",
"single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Google Cast."
"single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Google Cast."
},
"step": {
"confirm": {

View File

@ -6,7 +6,7 @@
},
"step": {
"confirm": {
"description": "M\u00f6chten Sie Google Cast einrichten?",
"description": "M\u00f6chtest du Google Cast einrichten?",
"title": "Google Cast"
}
},

View File

@ -2,12 +2,14 @@
"config": {
"abort": {
"no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.",
"single_instance_allowed": "Seulement une seule configuration de Google Cast est n\u00e9cessaire."
"single_instance_allowed": "Une seule configuration de Google Cast est n\u00e9cessaire."
},
"step": {
"confirm": {
"description": "Voulez-vous configurer Google Cast?"
"description": "Voulez-vous configurer Google Cast?",
"title": "Google Cast"
}
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Tidak ada perangkat Google Cast yang ditemukan pada jaringan.",
"single_instance_allowed": "Hanya satu konfigurasi Google Cast yang diperlukan."
},
"step": {
"confirm": {
"description": "Apakah Anda ingin menyiapkan Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Klar",
"single_instance_allowed": "Du treng berre \u00e5 sette opp \u00e9in Google Cast-konfigurasjon."
},
"step": {
"confirm": {
"description": "Vil du sette opp Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -35,4 +35,5 @@ async def _async_has_devices(hass):
config_entry_flow.register_discovery_flow(
DOMAIN, 'Google Cast', _async_has_devices)
DOMAIN, 'Google Cast', _async_has_devices,
config_entries.CONN_CLASS_LOCAL_PUSH)

View File

@ -10,7 +10,6 @@ import functools as ft
import voluptuous as vol
from homeassistant.loader import bind_hass
from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity_component import EntityComponent
@ -20,7 +19,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
PRECISION_TENTHS, )
PRECISION_TENTHS)
DEFAULT_MIN_TEMP = 7
DEFAULT_MAX_TEMP = 35
@ -142,107 +141,6 @@ SET_SWING_MODE_SCHEMA = vol.Schema({
})
@bind_hass
def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified climate devices away mode on."""
data = {
ATTR_AWAY_MODE: away_mode
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
@bind_hass
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)
@bind_hass
def set_aux_heat(hass, aux_heat, entity_id=None):
"""Turn all or specified climate devices auxiliary heater on."""
data = {
ATTR_AUX_HEAT: aux_heat
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
@bind_hass
def set_temperature(hass, temperature=None, entity_id=None,
target_temp_high=None, target_temp_low=None,
operation_mode=None):
"""Set new target temperature."""
kwargs = {
key: value for key, value in [
(ATTR_TEMPERATURE, temperature),
(ATTR_TARGET_TEMP_HIGH, target_temp_high),
(ATTR_TARGET_TEMP_LOW, target_temp_low),
(ATTR_ENTITY_ID, entity_id),
(ATTR_OPERATION_MODE, operation_mode)
] if value is not None
}
_LOGGER.debug("set_temperature start data=%s", kwargs)
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs)
@bind_hass
def set_humidity(hass, humidity, entity_id=None):
"""Set new target humidity."""
data = {ATTR_HUMIDITY: humidity}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data)
@bind_hass
def set_fan_mode(hass, fan, entity_id=None):
"""Set all or specified climate devices fan mode on."""
data = {ATTR_FAN_MODE: fan}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
@bind_hass
def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode."""
data = {ATTR_OPERATION_MODE: operation_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
@bind_hass
def set_swing_mode(hass, swing_mode, entity_id=None):
"""Set new target swing mode."""
data = {ATTR_SWING_MODE: swing_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
async def async_setup(hass, config):
"""Set up climate devices."""
component = hass.data[DOMAIN] = \

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyeconet==0.0.5']
REQUIREMENTS = ['pyeconet==0.0.6']
_LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,371 @@
"""Support for Honeywell evohome (EMEA/EU-based systems only).
Support for a temperature control system (TCS, controller) with 0+ heating
zones (e.g. TRVs, relays) and, optionally, a DHW controller.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.evohome/
"""
from datetime import datetime, timedelta
import logging
from requests.exceptions import HTTPError
from homeassistant.components.climate import (
ClimateDevice,
STATE_AUTO,
STATE_ECO,
STATE_OFF,
SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE,
)
from homeassistant.components.evohome import (
CONF_LOCATION_IDX,
DATA_EVOHOME,
MAX_TEMP,
MIN_TEMP,
SCAN_INTERVAL_MAX
)
from homeassistant.const import (
CONF_SCAN_INTERVAL,
PRECISION_TENTHS,
TEMP_CELSIUS,
HTTP_TOO_MANY_REQUESTS,
)
_LOGGER = logging.getLogger(__name__)
# these are for the controller's opmode/state and the zone's state
EVO_RESET = 'AutoWithReset'
EVO_AUTO = 'Auto'
EVO_AUTOECO = 'AutoWithEco'
EVO_AWAY = 'Away'
EVO_DAYOFF = 'DayOff'
EVO_CUSTOM = 'Custom'
EVO_HEATOFF = 'HeatingOff'
EVO_STATE_TO_HA = {
EVO_RESET: STATE_AUTO,
EVO_AUTO: STATE_AUTO,
EVO_AUTOECO: STATE_ECO,
EVO_AWAY: STATE_AUTO,
EVO_DAYOFF: STATE_AUTO,
EVO_CUSTOM: STATE_AUTO,
EVO_HEATOFF: STATE_OFF
}
HA_STATE_TO_EVO = {
STATE_AUTO: EVO_AUTO,
STATE_ECO: EVO_AUTOECO,
STATE_OFF: EVO_HEATOFF
}
HA_OP_LIST = list(HA_STATE_TO_EVO)
# these are used to help prevent E501 (line too long) violations
GWS = 'gateways'
TCS = 'temperatureControlSystems'
# debug codes - these happen occasionally, but the cause is unknown
EVO_DEBUG_NO_RECENT_UPDATES = '0x01'
EVO_DEBUG_NO_STATUS = '0x02'
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create a Honeywell (EMEA/EU) evohome CH/DHW system.
An evohome system consists of: a controller, with 0-12 heating zones (e.g.
TRVs, relays) and, optionally, a DHW controller (a HW boiler).
Here, we add the controller only.
"""
evo_data = hass.data[DATA_EVOHOME]
client = evo_data['client']
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
# evohomeclient has no defined way of accessing non-default location other
# than using a protected member, such as below
tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa E501; pylint: disable=protected-access
_LOGGER.debug(
"setup_platform(): Found Controller: id: %s [%s], type: %s",
tcs_obj_ref.systemId,
tcs_obj_ref.location.name,
tcs_obj_ref.modelType
)
parent = EvoController(evo_data, client, tcs_obj_ref)
add_entities([parent], update_before_add=True)
class EvoController(ClimateDevice):
"""Base for a Honeywell evohome hub/Controller device.
The Controller (aka TCS, temperature control system) is the parent of all
the child (CH/DHW) devices.
"""
def __init__(self, evo_data, client, obj_ref):
"""Initialize the evohome entity.
Most read-only properties are set here. So are pseudo read-only,
for example name (which _could_ change between update()s).
"""
self.client = client
self._obj = obj_ref
self._id = obj_ref.systemId
self._name = evo_data['config']['locationInfo']['name']
self._config = evo_data['config'][GWS][0][TCS][0]
self._params = evo_data['params']
self._timers = evo_data['timers']
self._timers['statusUpdated'] = datetime.min
self._status = {}
self._available = False # should become True after first update()
def _handle_requests_exceptions(self, err):
# evohomeclient v2 api (>=0.2.7) exposes requests exceptions, incl.:
# - HTTP_BAD_REQUEST, is usually Bad user credentials
# - HTTP_TOO_MANY_REQUESTS, is api usuage limit exceeded
# - HTTP_SERVICE_UNAVAILABLE, is often Vendor's fault
if err.response.status_code == HTTP_TOO_MANY_REQUESTS:
# execute a back off: pause, and reduce rate
old_scan_interval = self._params[CONF_SCAN_INTERVAL]
new_scan_interval = min(old_scan_interval * 2, SCAN_INTERVAL_MAX)
self._params[CONF_SCAN_INTERVAL] = new_scan_interval
_LOGGER.warning(
"API rate limit has been exceeded: increasing '%s' from %s to "
"%s seconds, and suspending polling for %s seconds.",
CONF_SCAN_INTERVAL,
old_scan_interval,
new_scan_interval,
new_scan_interval * 3
)
self._timers['statusUpdated'] = datetime.now() + \
timedelta(seconds=new_scan_interval * 3)
else:
raise err
@property
def name(self):
"""Return the name to use in the frontend UI."""
return self._name
@property
def available(self):
"""Return True if the device is available.
All evohome entities are initially unavailable. Once HA has started,
state data is then retrieved by the Controller, and then the children
will get a state (e.g. operating_mode, current_temperature).
However, evohome entities can become unavailable for other reasons.
"""
return self._available
@property
def supported_features(self):
"""Get the list of supported features of the Controller."""
return SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE
@property
def device_state_attributes(self):
"""Return the device state attributes of the controller.
This is operating mode state data that is not available otherwise, due
to the restrictions placed upon ClimateDevice properties, etc by HA.
"""
data = {}
data['systemMode'] = self._status['systemModeStatus']['mode']
data['isPermanent'] = self._status['systemModeStatus']['isPermanent']
if 'timeUntil' in self._status['systemModeStatus']:
data['timeUntil'] = self._status['systemModeStatus']['timeUntil']
data['activeFaults'] = self._status['activeFaults']
return data
@property
def operation_list(self):
"""Return the list of available operations."""
return HA_OP_LIST
@property
def current_operation(self):
"""Return the operation mode of the evohome entity."""
return EVO_STATE_TO_HA.get(self._status['systemModeStatus']['mode'])
@property
def target_temperature(self):
"""Return the average target temperature of the Heating/DHW zones."""
temps = [zone['setpointStatus']['targetHeatTemperature']
for zone in self._status['zones']]
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
return avg_temp
@property
def current_temperature(self):
"""Return the average current temperature of the Heating/DHW zones."""
tmp_list = [x for x in self._status['zones']
if x['temperatureStatus']['isAvailable'] is True]
temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list]
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
return avg_temp
@property
def temperature_unit(self):
"""Return the temperature unit to use in the frontend UI."""
return TEMP_CELSIUS
@property
def precision(self):
"""Return the temperature precision to use in the frontend UI."""
return PRECISION_TENTHS
@property
def min_temp(self):
"""Return the minimum target temp (setpoint) of a evohome entity."""
return MIN_TEMP
@property
def max_temp(self):
"""Return the maximum target temp (setpoint) of a evohome entity."""
return MAX_TEMP
@property
def is_on(self):
"""Return true as evohome controllers are always on.
Operating modes can include 'HeatingOff', but (for example) DHW would
remain on.
"""
return True
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._status['systemModeStatus']['mode'] == EVO_AWAY
def turn_away_mode_on(self):
"""Turn away mode on."""
self._set_operation_mode(EVO_AWAY)
def turn_away_mode_off(self):
"""Turn away mode off."""
self._set_operation_mode(EVO_AUTO)
def _set_operation_mode(self, operation_mode):
# Set new target operation mode for the TCS.
_LOGGER.debug(
"_set_operation_mode(): API call [1 request(s)]: "
"tcs._set_status(%s)...",
operation_mode
)
try:
self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access
except HTTPError as err:
self._handle_requests_exceptions(err)
def set_operation_mode(self, operation_mode):
"""Set new target operation mode for the TCS.
Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away'
mode is needed, it can be enabled via turn_away_mode_on method.
"""
self._set_operation_mode(HA_STATE_TO_EVO.get(operation_mode))
def _update_state_data(self, evo_data):
client = evo_data['client']
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
_LOGGER.debug(
"_update_state_data(): API call [1 request(s)]: "
"client.locations[loc_idx].status()..."
)
try:
evo_data['status'].update(
client.locations[loc_idx].status()[GWS][0][TCS][0])
except HTTPError as err: # check if we've exceeded the api rate limit
self._handle_requests_exceptions(err)
else:
evo_data['timers']['statusUpdated'] = datetime.now()
_LOGGER.debug(
"_update_state_data(): evo_data['status'] = %s",
evo_data['status']
)
def update(self):
"""Get the latest state data of the installation.
This includes state data for the Controller and its child devices, such
as the operating_mode of the Controller and the current_temperature
of its children.
This is not asyncio-friendly due to the underlying client api.
"""
evo_data = self.hass.data[DATA_EVOHOME]
timeout = datetime.now() + timedelta(seconds=55)
expired = timeout > self._timers['statusUpdated'] + \
timedelta(seconds=evo_data['params'][CONF_SCAN_INTERVAL])
if not expired:
return
was_available = self._available or \
self._timers['statusUpdated'] == datetime.min
self._update_state_data(evo_data)
self._status = evo_data['status']
if _LOGGER.isEnabledFor(logging.DEBUG):
tmp_dict = dict(self._status)
if 'zones' in tmp_dict:
tmp_dict['zones'] = '...'
if 'dhw' in tmp_dict:
tmp_dict['dhw'] = '...'
_LOGGER.debug(
"update(%s), self._status = %s",
self._id + " [" + self._name + "]",
tmp_dict
)
no_recent_updates = self._timers['statusUpdated'] < datetime.now() - \
timedelta(seconds=self._params[CONF_SCAN_INTERVAL] * 3.1)
if no_recent_updates:
self._available = False
debug_code = EVO_DEBUG_NO_RECENT_UPDATES
elif not self._status:
# unavailable because no status (but how? other than at startup?)
self._available = False
debug_code = EVO_DEBUG_NO_STATUS
else:
self._available = True
if not self._available and was_available:
# only warn if available went from True to False
_LOGGER.warning(
"The entity, %s, has become unavailable, debug code is: %s",
self._id + " [" + self._name + "]",
debug_code
)
elif self._available and not was_available:
# this isn't the first re-available (e.g. _after_ STARTUP)
_LOGGER.debug(
"The entity, %s, has become available",
self._id + " [" + self._name + "]"
)

View File

@ -251,6 +251,14 @@ class GenericThermostat(ClimateDevice):
# Ensure we update the current operation after changing the mode
self.schedule_update_ha_state()
async def async_turn_on(self):
"""Turn thermostat on."""
await self.async_set_operation_mode(self.operation_list[0])
async def async_turn_off(self):
"""Turn thermostat off."""
await self.async_set_operation_mode(STATE_OFF)
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)

View File

@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE, CONF_REGION)
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2']
REQUIREMENTS = ['evohomeclient==0.2.7', 'somecomfort==0.5.2']
_LOGGER = logging.getLogger(__name__)

View File

@ -10,7 +10,7 @@ import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import mqtt
from homeassistant.components import mqtt, climate
from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
@ -21,9 +21,13 @@ from homeassistant.components.climate import (
from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE)
from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability)
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH)
@ -126,13 +130,28 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the MQTT climate devices."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None):
"""Set up MQTT climate device through configuration.yaml."""
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up MQTT climate device dynamically through MQTT discovery."""
async def async_discover(discovery_payload):
"""Discover and add a MQTT climate device."""
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(hass, config, async_add_entities,
discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(climate.DOMAIN, 'mqtt'),
async_discover)
async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None):
"""Set up the MQTT climate devices."""
template_keys = (
CONF_POWER_STATE_TEMPLATE,
CONF_MODE_STATE_TEMPLATE,
@ -194,11 +213,12 @@ def async_setup_platform(hass, config, async_add_entities,
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_MIN_TEMP),
config.get(CONF_MAX_TEMP))
])
config.get(CONF_MAX_TEMP),
discovery_hash,
)])
class MqttClimate(MqttAvailability, ClimateDevice):
class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
"""Representation of an MQTT climate device."""
def __init__(self, hass, name, topic, value_templates, qos, retain,
@ -207,10 +227,11 @@ class MqttClimate(MqttAvailability, ClimateDevice):
current_swing_mode, current_operation, aux, send_if_off,
payload_on, payload_off, availability_topic,
payload_available, payload_not_available,
min_temp, max_temp):
min_temp, max_temp, discovery_hash):
"""Initialize the climate device."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
self.hass = hass
self._name = name
self._topic = topic
@ -235,11 +256,13 @@ class MqttClimate(MqttAvailability, ClimateDevice):
self._payload_off = payload_off
self._min_temp = min_temp
self._max_temp = max_temp
self._discovery_hash = discovery_hash
@asyncio.coroutine
def async_added_to_hass(self):
"""Handle being added to home assistant."""
yield from super().async_added_to_hass()
yield from MqttAvailability.async_added_to_hass(self)
yield from MqttDiscoveryUpdate.async_added_to_hass(self)
@callback
def handle_current_temp_received(topic, payload, qos):

View File

@ -0,0 +1,190 @@
"""
Support for OpenTherm Gateway devices.
For more details about this component, please refer to the documentation at
http://home-assistant.io/components/climate.opentherm_gw/
"""
import logging
import voluptuous as vol
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA,
STATE_IDLE, STATE_HEAT,
STATE_COOL,
SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME,
PRECISION_HALVES, PRECISION_TENTHS,
TEMP_CELSIUS, PRECISION_WHOLE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyotgw==0.1b0']
CONF_FLOOR_TEMP = "floor_temperature"
CONF_PRECISION = 'precision'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICE): cv.string,
vol.Optional(CONF_NAME, default="OpenTherm Gateway"): cv.string,
vol.Optional(CONF_PRECISION): vol.In([PRECISION_TENTHS, PRECISION_HALVES,
PRECISION_WHOLE]),
vol.Optional(CONF_FLOOR_TEMP, default=False): cv.boolean,
})
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the opentherm_gw device."""
gateway = OpenThermGateway(config)
async_add_entities([gateway])
class OpenThermGateway(ClimateDevice):
"""Representation of a climate device."""
def __init__(self, config):
"""Initialize the sensor."""
import pyotgw
self.pyotgw = pyotgw
self.gateway = self.pyotgw.pyotgw()
self._device = config[CONF_DEVICE]
self.friendly_name = config.get(CONF_NAME)
self.floor_temp = config.get(CONF_FLOOR_TEMP)
self.temp_precision = config.get(CONF_PRECISION)
self._current_operation = STATE_IDLE
self._current_temperature = 0.0
self._target_temperature = 0.0
self._away_mode_a = None
self._away_mode_b = None
self._away_state_a = False
self._away_state_b = False
async def async_added_to_hass(self):
"""Connect to the OpenTherm Gateway device."""
await self.gateway.connect(self.hass.loop, self._device)
self.gateway.subscribe(self.receive_report)
_LOGGER.debug("Connected to %s on %s", self.friendly_name,
self._device)
async def receive_report(self, status):
"""Receive and handle a new report from the Gateway."""
_LOGGER.debug("Received report: %s", status)
ch_active = status.get(self.pyotgw.DATA_SLAVE_CH_ACTIVE)
flame_on = status.get(self.pyotgw.DATA_SLAVE_FLAME_ON)
cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE)
if ch_active and flame_on:
self._current_operation = STATE_HEAT
elif cooling_active:
self._current_operation = STATE_COOL
else:
self._current_operation = STATE_IDLE
self._current_temperature = status.get(self.pyotgw.DATA_ROOM_TEMP)
temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT_OVRD)
if temp is None:
temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT)
self._target_temperature = temp
# GPIO mode 5: 0 == Away
# GPIO mode 6: 1 == Away
gpio_a_state = status.get(self.pyotgw.OTGW_GPIO_A)
if gpio_a_state == 5:
self._away_mode_a = 0
elif gpio_a_state == 6:
self._away_mode_a = 1
else:
self._away_mode_a = None
gpio_b_state = status.get(self.pyotgw.OTGW_GPIO_B)
if gpio_b_state == 5:
self._away_mode_b = 0
elif gpio_b_state == 6:
self._away_mode_b = 1
else:
self._away_mode_b = None
if self._away_mode_a is not None:
self._away_state_a = (status.get(self.pyotgw.OTGW_GPIO_A_STATE) ==
self._away_mode_a)
if self._away_mode_b is not None:
self._away_state_b = (status.get(self.pyotgw.OTGW_GPIO_B_STATE) ==
self._away_mode_b)
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the friendly name."""
return self.friendly_name
@property
def precision(self):
"""Return the precision of the system."""
if self.temp_precision is not None:
return self.temp_precision
if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
return PRECISION_HALVES
return PRECISION_WHOLE
@property
def should_poll(self):
"""Disable polling for this entity."""
return False
@property
def temperature_unit(self):
"""Return the unit of measurement used by the platform."""
return TEMP_CELSIUS
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_operation
@property
def current_temperature(self):
"""Return the current temperature."""
if self.floor_temp is True:
if self.temp_precision == PRECISION_HALVES:
return int(2 * self._current_temperature) / 2
if self.temp_precision == PRECISION_TENTHS:
return int(10 * self._current_temperature) / 10
return int(self._current_temperature)
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return self.temp_precision
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away_state_a or self._away_state_b
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs:
temp = float(kwargs[ATTR_TEMPERATURE])
self._target_temperature = await self.gateway.set_target_temp(
temp)
self.async_schedule_update_ha_state()
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def min_temp(self):
"""Return the minimum temperature."""
return 1
@property
def max_temp(self):
"""Return the maximum temperature."""
return 30

View File

@ -174,8 +174,8 @@ class RadioThermostat(ClimateDevice):
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
ATTR_FAN: self._fmode,
ATTR_MODE: self._tmode,
ATTR_FAN: self._fstate,
ATTR_MODE: self._tstate,
}
@property

View File

@ -118,7 +118,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
self.hass, self.target_temperature_low, self.temperature_unit,
PRECISION_TENTHS)
if self.external_temperature:
if self.external_temperature is not None:
data[ATTR_EXTERNAL_TEMPERATURE] = show_temp(
self.hass, self.external_temperature, self.temperature_unit,
PRECISION_TENTHS)
@ -126,16 +126,16 @@ class WinkThermostat(WinkDevice, ClimateDevice):
if self.smart_temperature:
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
if self.occupied:
if self.occupied is not None:
data[ATTR_OCCUPIED] = self.occupied
if self.eco_target:
if self.eco_target is not None:
data[ATTR_ECO_TARGET] = self.eco_target
if self.heat_on:
if self.heat_on is not None:
data[ATTR_HEAT_ON] = self.heat_on
if self.cool_on:
if self.cool_on is not None:
data[ATTR_COOL_ON] = self.cool_on
current_humidity = self.current_humidity

View File

@ -10,8 +10,8 @@ from homeassistant.components.climate import (
DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
ZWaveDeviceEntity, async_setup_platform)
from homeassistant.const import (
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)

View File

@ -23,12 +23,17 @@ from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c
from . import http_api, iot
from . import http_api, iot, auth_api
from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.6.1']
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
STORAGE_ENABLE_ALEXA = 'alexa_enabled'
STORAGE_ENABLE_GOOGLE = 'google_enabled'
_LOGGER = logging.getLogger(__name__)
_UNDEF = object()
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'
@ -39,6 +44,7 @@ CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
@ -79,6 +85,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
vol.Optional(CONF_SUBSCRIPTION_INFO_URL): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}),
@ -114,18 +121,21 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None,
relayer=None, google_actions_sync_url=None):
relayer=None, google_actions_sync_url=None,
subscription_info_url=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
self.alexa_config = alexa
self._google_actions = google_actions
self._gactions_config = None
self._prefs = None
self.jwt_keyset = None
self.id_token = None
self.access_token = None
self.refresh_token = None
self.iot = iot.CloudIoT(self)
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id
@ -133,6 +143,7 @@ class Cloud:
self.region = region
self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url
self.subscription_info_url = subscription_info_url
else:
info = SERVERS[mode]
@ -142,6 +153,7 @@ class Cloud:
self.region = info['region']
self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url']
self.subscription_info_url = info['subscription_info_url']
@property
def is_logged_in(self):
@ -188,6 +200,16 @@ class Cloud:
return self._gactions_config
@property
def alexa_enabled(self):
"""Return if Alexa is enabled."""
return self._prefs[STORAGE_ENABLE_ALEXA]
@property
def google_enabled(self):
"""Return if Google is enabled."""
return self._prefs[STORAGE_ENABLE_GOOGLE]
def path(self, *parts):
"""Get config path inside cloud dir.
@ -195,6 +217,15 @@ class Cloud:
"""
return self.hass.config.path(CONFIG_DIR, *parts)
async def fetch_subscription_info(self):
"""Fetch subscription info."""
await self.hass.async_add_executor_job(auth_api.check_token, self)
websession = self.hass.helpers.aiohttp_client.async_get_clientsession()
return await websession.get(
self.subscription_info_url, headers={
'authorization': self.id_token
})
@asyncio.coroutine
def logout(self):
"""Close connection and remove all credentials."""
@ -217,10 +248,23 @@ class Cloud:
'refresh_token': self.refresh_token,
}, indent=4))
@asyncio.coroutine
def async_start(self, _):
async def async_start(self, _):
"""Start the cloud component."""
success = yield from self._fetch_jwt_keyset()
prefs = await self._store.async_load()
if prefs is None:
prefs = {}
if self.mode not in prefs:
# Default to True if already logged in to make this not a
# breaking change.
enabled = await self.hass.async_add_executor_job(
os.path.isfile, self.user_info_path)
prefs = {
STORAGE_ENABLE_ALEXA: enabled,
STORAGE_ENABLE_GOOGLE: enabled,
}
self._prefs = prefs
success = await self._fetch_jwt_keyset()
# Fetching keyset can fail if internet is not up yet.
if not success:
@ -241,7 +285,7 @@ class Cloud:
with open(user_info, 'rt') as file:
return json.loads(file.read())
info = yield from self.hass.async_add_job(load_config)
info = await self.hass.async_add_job(load_config)
if info is None:
return
@ -260,6 +304,15 @@ class Cloud:
self.hass.add_job(self.iot.connect())
async def update_preferences(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF):
"""Update user preferences."""
if google_enabled is not _UNDEF:
self._prefs[STORAGE_ENABLE_GOOGLE] = google_enabled
if alexa_enabled is not _UNDEF:
self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled
await self._store.async_save(self._prefs)
@asyncio.coroutine
def _fetch_jwt_keyset(self):
"""Fetch the JWT keyset for the Cognito instance."""

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