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

View File

@ -3,7 +3,7 @@
**Related issue (if applicable):** fixes #<home-assistant issue number goes here> **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): ## Example entry for `configuration.yaml` (if applicable):
```yaml ```yaml
@ -15,7 +15,7 @@
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
If user exposed functionality or configuration variables are added/changed: 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: If the code communicates with devices, web services, or third-party tools:
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] 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/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/huawei_router.py @abmantis
homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/history_graph.py @andrey-git homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/lifx.py @amelchio 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/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 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/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/nsw_fuel_station.py @nickw444 homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/pollen.py @bachya
@ -91,11 +93,15 @@ homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610 homeassistant/components/*/deconz.py @kane610
homeassistant/components/ecovacs.py @OverloadUT homeassistant/components/ecovacs.py @OverloadUT
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/*/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p 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/*/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342
homeassistant/components/konnected.py @heythisisnate 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/*/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen 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/*/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
homeassistant/components/zoneminder.py @rohankapoorcom
homeassistant/components/*/zoneminder.py @rohankapoorcom
homeassistant/scripts/check_config.py @kellerza homeassistant/scripts/check_config.py @kellerza

View File

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

View File

@ -21,8 +21,8 @@ Featured integrations
|screenshot-components| |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 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://home-assistant.io/developers/creating_components/>`__. components <https://developers.home-assistant.io/docs/en/creating_component_index.html>`__.
If you run into issues while using Home Assistant or during development If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information. 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:`modindex`
* :ref:`search` * :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 subprocess
import sys import sys
import threading import threading
from typing import List, Dict, Any # noqa pylint: disable=unused-import 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.""" """Attempt to use uvloop."""
import asyncio import asyncio
if sys.platform == 'win32':
asyncio.set_event_loop(asyncio.ProactorEventLoop())
else:
try: try:
import uvloop import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError: except ImportError:
pass pass
else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
def validate_python() -> None: def validate_python() -> None:
@ -240,51 +243,39 @@ def cmdline() -> List[str]:
return [arg for arg in sys.argv if arg != '--daemon'] return [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir: str, async def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> int: args: argparse.Namespace) -> int:
"""Set up HASS and run.""" """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 hass = core.HomeAssistant()
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)
if args.demo_mode: if args.demo_mode:
config = { config = {
'frontend': {}, 'frontend': {},
'demo': {} 'demo': {}
} # type: Dict[str, Any] } # type: Dict[str, Any]
hass = bootstrap.from_config_dict( bootstrap.async_from_config_dict(
config, config_dir=config_dir, verbose=args.verbose, config, hass, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
log_file=args.log_file, log_no_color=args.log_no_color) log_file=args.log_file, log_no_color=args.log_no_color)
else: else:
config_file = ensure_config_file(config_dir) config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir) print('Config directory:', config_dir)
hass = bootstrap.from_config_file( await bootstrap.async_from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip, config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days, log_file=args.log_file, log_rotate_days=args.log_rotate_days, log_file=args.log_file,
log_no_color=args.log_no_color) log_no_color=args.log_no_color)
if hass is None:
return -1
if args.open_ui: if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch # Imported here to avoid importing asyncio before monkey patch
from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
def open_browser(_: Any) -> None: def open_browser(_: Any) -> None:
"""Open the web interface in a browser.""" """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 import webbrowser
webbrowser.open(hass.config.api.base_url) # type: ignore webbrowser.open(hass.config.api.base_url)
run_callback_threadsafe( run_callback_threadsafe(
hass.loop, hass.loop,
@ -292,7 +283,7 @@ def setup_and_run_hass(config_dir: str,
EVENT_HOMEASSISTANT_START, open_browser EVENT_HOMEASSISTANT_START, open_browser
) )
return hass.start() return await hass.async_run()
def try_to_restart() -> None: def try_to_restart() -> None:
@ -347,7 +338,20 @@ def main() -> int:
monkey_patch.disable_c_asyncio() monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks() 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() args = get_arguments()
@ -366,11 +370,12 @@ def main() -> int:
if args.pid_file: if args.pid_file:
write_pid(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: if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart() try_to_restart()
return exit_code return exit_code # type: ignore # mypy cannot yet infer it
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -2,11 +2,13 @@
import asyncio import asyncio
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple, cast from typing import Any, Dict, List, Optional, Tuple, cast
import jwt import jwt
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import callback, HomeAssistant from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -242,8 +244,12 @@ class AuthManager:
modules[module_id] = module.name modules[module_id] = module.name
return modules return modules
async def async_create_refresh_token(self, user: models.User, async def async_create_refresh_token(
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: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken: -> models.RefreshToken:
"""Create a new refresh token for a user.""" """Create a new refresh token for a user."""
if not user.is_active: if not user.is_active:
@ -254,10 +260,36 @@ class AuthManager:
'System generated users cannot have refresh tokens connected ' 'System generated users cannot have refresh tokens connected '
'to a client.') '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.') 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( async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]: self, token_id: str) -> Optional[models.RefreshToken]:
@ -277,13 +309,17 @@ class AuthManager:
@callback @callback
def async_create_access_token(self, 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.""" """Create a new access token."""
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
# pylint: disable=no-self-use # pylint: disable=no-self-use
now = dt_util.utcnow()
return jwt.encode({ return jwt.encode({
'iss': refresh_token.id, 'iss': refresh_token.id,
'iat': dt_util.utcnow(), 'iat': now,
'exp': dt_util.utcnow() + refresh_token.access_token_expiration, 'exp': now + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode() }, refresh_token.jwt_key, algorithm='HS256').decode()
async def async_validate_access_token( 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 from typing import Any, Dict, List, Optional # noqa: F401
import hmac import hmac
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -27,7 +28,8 @@ class AuthStore:
"""Initialize the auth store.""" """Initialize the auth store."""
self.hass = hass self.hass = hass
self._users = None # type: Optional[Dict[str, models.User]] 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]: async def async_get_users(self) -> List[models.User]:
"""Retrieve all users.""" """Retrieve all users."""
@ -128,11 +130,27 @@ class AuthStore:
self._async_schedule_save() self._async_schedule_save()
async def async_create_refresh_token( 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: -> models.RefreshToken:
"""Create a new token for a user.""" """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 user.refresh_tokens[refresh_token.id] = refresh_token
self._async_schedule_save() self._async_schedule_save()
return refresh_token return refresh_token
@ -178,6 +196,15 @@ class AuthStore:
return found 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: async def _async_load(self) -> None:
"""Load the users.""" """Load the users."""
data = await self._store.async_load() data = await self._store.async_load()
@ -216,15 +243,36 @@ class AuthStore:
'Ignoring refresh token %(id)s with invalid created_at ' 'Ignoring refresh token %(id)s with invalid created_at '
'%(created_at)s for user_id %(user_id)s', rt_dict) '%(created_at)s for user_id %(user_id)s', rt_dict)
continue 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( token = models.RefreshToken(
id=rt_dict['id'], id=rt_dict['id'],
user=users[rt_dict['user_id']], user=users[rt_dict['user_id']],
client_id=rt_dict['client_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, created_at=created_at,
access_token_expiration=timedelta( access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']), seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'], 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 users[rt_dict['user_id']].refresh_tokens[token.id] = token
@ -271,11 +319,18 @@ class AuthStore:
'id': refresh_token.id, 'id': refresh_token.id,
'user_id': user.id, 'user_id': user.id,
'client_id': refresh_token.client_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(), 'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration': 'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(), refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token, 'token': refresh_token.token,
'jwt_key': refresh_token.jwt_key, '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 user in self._users.values()
for refresh_token in user.refresh_tokens.values() for refresh_token in user.refresh_tokens.values()

View File

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

View File

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

View File

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

View File

@ -7,9 +7,12 @@ import attr
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ACCESS_TOKEN_EXPIRATION
from .util import generate_secret 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) @attr.s(slots=True)
class User: class User:
@ -37,23 +40,31 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens.""" """RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User) 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)) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) 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, token = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64))) default=attr.Factory(lambda: generate_secret(64)))
jwt_key = attr.ib(type=str, jwt_key = attr.ib(type=str,
default=attr.Factory(lambda: generate_secret(64))) 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) @attr.s(slots=True)
class Credentials: class Credentials:
"""Credentials for a user on an auth provider.""" """Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str) 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. # Allow the auth provider to store data to represent their auth.
data = attr.ib(type=dict) 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 homeassistant.util.decorator import Registry
from ..auth_store import AuthStore from ..auth_store import AuthStore
from ..const import MFA_SESSION_EXPIRATION
from ..models import Credentials, User, UserMeta # noqa: F401 from ..models import Credentials, User, UserMeta # noqa: F401
from ..mfa_modules import SESSION_EXPIRATION
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed' 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._auth_manager = auth_provider.hass.auth # type: ignore
self.available_mfa_modules = {} # type: Dict[str, str] self.available_mfa_modules = {} # type: Dict[str, str]
self.created_at = dt_util.utcnow() self.created_at = dt_util.utcnow()
self.invalid_mfa_times = 0
self.user = None # type: Optional[User] self.user = None # type: Optional[User]
async def async_step_init( async def async_step_init(
@ -212,6 +213,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
self, user_input: Optional[Dict[str, str]] = None) \ self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]: -> Dict[str, Any]:
"""Handle the step of mfa validation.""" """Handle the step of mfa validation."""
assert self.user
errors = {} errors = {}
auth_module = self._auth_manager.get_auth_mfa_module( 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 # will show invalid_auth_module error
return await self.async_step_select_mfa_module(user_input={}) 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: if user_input is not None:
expires = self.created_at + SESSION_EXPIRATION expires = self.created_at + MFA_SESSION_EXPIRATION
if dt_util.utcnow() > expires: if dt_util.utcnow() > expires:
return self.async_abort( return self.async_abort(
reason='login_expired' reason='login_expired'
) )
result = await auth_module.async_validation( result = await auth_module.async_validate(
self.user.id, user_input) # type: ignore self.user.id, user_input)
if not result: if not result:
errors['base'] = 'invalid_code' 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: if not errors:
return await self.async_finish(self.user) return await self.async_finish(self.user)
description_placeholders = { description_placeholders = {
'mfa_module_name': auth_module.name, 'mfa_module_name': auth_module.name,
'mfa_module_id': auth_module.id 'mfa_module_id': auth_module.id,
} # type: Dict[str, str] } # type: Dict[str, Optional[str]]
return self.async_show_form( return self.async_show_form(
step_id='mfa', step_id='mfa',

View File

@ -52,7 +52,8 @@ class Data:
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the user data store.""" """Initialize the user data store."""
self.hass = hass 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]] self._data = None # type: Optional[Dict[str, Any]]
async def async_load(self) -> None: async def async_load(self) -> None:

View File

@ -24,7 +24,7 @@ USER_SCHEMA = vol.Schema({
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA) }, extra=vol.PREVENT_EXTRA)
LEGACY_USER = 'homeassistant' LEGACY_USER_NAME = 'Legacy API password user'
class InvalidAuthError(HomeAssistantError): class InvalidAuthError(HomeAssistantError):
@ -52,23 +52,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
async def async_get_or_create_credentials( async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials: self, flow_result: Dict[str, str]) -> Credentials:
"""Return LEGACY_USER always.""" """Return credentials for this login."""
for credential in await self.async_credentials(): credentials = await self.async_credentials()
if credential.data['username'] == LEGACY_USER: if credentials:
return credential return credentials[0]
return self.async_create_credentials({ return self.async_create_credentials({})
'username': LEGACY_USER
})
async def async_user_meta_for_credentials( async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta: 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. 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): class LegacyLoginFlow(LoginFlow):

View File

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

View File

@ -14,7 +14,6 @@ from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
from homeassistant.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity 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 @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Track states and offer events for sensors.""" """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, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
CONF_NAME, CONF_CODE) CONF_NAME, CONF_CODE)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_RETAIN, MqttAvailability) CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv 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__) _LOGGER = logging.getLogger(__name__)
@ -46,13 +49,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
def async_setup_platform(hass, config, async_add_entities, async_add_entities, discovery_info=None):
discovery_info=None): """Set up MQTT alarm control panel through configuration.yaml."""
"""Set up the MQTT Alarm Control Panel platform.""" await _async_setup_entity(hass, config, async_add_entities)
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
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( async_add_entities([MqttAlarm(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), 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_CODE),
config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE), 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.""" """Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, retain, def __init__(self, name, state_topic, command_topic, qos, retain,
payload_disarm, payload_arm_home, payload_arm_away, code, 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.""" """Init the MQTT Alarm Control Panel."""
super().__init__(availability_topic, qos, payload_available, MqttAvailability.__init__(self, availability_topic, qos,
payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._name = name self._name = name
self._state_topic = state_topic self._state_topic = state_topic
@ -87,11 +109,13 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
self._payload_arm_home = payload_arm_home self._payload_arm_home = payload_arm_home
self._payload_arm_away = payload_arm_away self._payload_arm_away = payload_arm_away
self._code = code self._code = code
self._discovery_hash = discovery_hash
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Subscribe mqtt events.""" """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 @callback
def message_received(topic, payload, qos): 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.spc/ https://home-assistant.io/components/alarm_control_panel.spc/
""" """
import asyncio
import logging import logging
import homeassistant.components.alarm_control_panel as alarm 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 ( 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 ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_UNKNOWN) STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__) _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(area):
def _get_alarm_state(spc_mode):
"""Get the alarm state.""" """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 async def async_setup_platform(hass, config, async_add_entities,
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up the SPC alarm control panel platform.""" """Set up the SPC alarm control panel platform."""
if (discovery_info is None or if (discovery_info is None or
discovery_info[ATTR_DISCOVER_AREAS] is None): discovery_info[ATTR_DISCOVER_AREAS] is None):
return return
api = hass.data[DATA_API] async_add_entities([SpcAlarm(area=area, api=hass.data[DATA_API])
devices = [SpcAlarm(api, area) for area in discovery_info[ATTR_DISCOVER_AREAS]])
for area in discovery_info[ATTR_DISCOVER_AREAS]]
async_add_entities(devices)
class SpcAlarm(alarm.AlarmControlPanel): class SpcAlarm(alarm.AlarmControlPanel):
"""Representation of the SPC alarm panel.""" """Representation of the SPC alarm panel."""
def __init__(self, api, area): def __init__(self, area, api):
"""Initialize the SPC alarm panel.""" """Initialize the SPC alarm panel."""
self._area_id = area['id'] self._area = area
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._api = api self._api = api
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Call for adding new entities.""" """Call for adding new entities."""
self.hass.data[DATA_REGISTRY].register_alarm_device( async_dispatcher_connect(self.hass,
self._area_id, self) SIGNAL_UPDATE_ALARM.format(self._area.id),
self._update_callback)
@asyncio.coroutine @callback
def async_update_from_spc(self, state, extra): def _update_callback(self):
"""Update the alarm panel with a new state.""" """Call update method."""
self._state = state self.async_schedule_update_ha_state(True)
self._changed_by = extra.get('changed_by', 'unknown')
self.async_schedule_update_ha_state()
@property @property
def should_poll(self): def should_poll(self):
@ -78,32 +72,34 @@ class SpcAlarm(alarm.AlarmControlPanel):
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._area.name
@property @property
def changed_by(self): def changed_by(self):
"""Return the user the last change was triggered by.""" """Return the user the last change was triggered by."""
return self._changed_by return self._area.last_changed_by
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
return self._state return _get_alarm_state(self._area)
@asyncio.coroutine async def async_alarm_disarm(self, code=None):
def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
yield from self._api.send_area_command( from pyspcwebgw.const import AreaMode
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET) self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET)
@asyncio.coroutine async def async_alarm_arm_home(self, code=None):
def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
yield from self._api.send_area_command( from pyspcwebgw.const import AreaMode
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET) self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A)
@asyncio.coroutine async def async_alarm_arm_night(self, code=None):
def async_alarm_arm_away(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.""" """Send arm away command."""
yield from self._api.send_area_command( from pyspcwebgw.const import AreaMode
self._area_id, SpcWebGateway.AREA_COMMAND_SET) 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', name='StateReport',
context={'properties': properties} 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 asyncio
import logging import logging
from typing import Sequence, TypeVar, Union from typing import Sequence, TypeVar, Union
import voluptuous as vol import voluptuous as vol

View File

@ -61,10 +61,12 @@ def setup(hass, config):
arlo_base_station = next(( arlo_base_station = next((
station for station in arlo.base_stations), None) 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 return False
arlo_base_station.refresh_rate = scan_interval.total_seconds()
hass.data[DATA_ARLO] = arlo hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex: except (ConnectTimeout, HTTPError) as ex:

View File

@ -13,7 +13,7 @@ from homeassistant.core import callback
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send) async_dispatcher_send, dispatcher_connect)
REQUIREMENTS = ['asterisk_mbox==0.5.0'] REQUIREMENTS = ['asterisk_mbox==0.5.0']
@ -21,8 +21,11 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'asterisk_mbox' DOMAIN = 'asterisk_mbox'
SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform"
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' 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({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -41,9 +44,7 @@ def setup(hass, config):
port = conf.get(CONF_PORT) port = conf.get(CONF_PORT)
password = conf.get(CONF_PASSWORD) password = conf.get(CONF_PASSWORD)
hass.data[DOMAIN] = AsteriskData(hass, host, port, password) hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config)
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
return True return True
@ -51,31 +52,71 @@ def setup(hass, config):
class AsteriskData: class AsteriskData:
"""Store Asterisk mailbox data.""" """Store Asterisk mailbox data."""
def __init__(self, hass, host, port, password): def __init__(self, hass, host, port, password, config):
"""Init the Asterisk data object.""" """Init the Asterisk data object."""
from asterisk_mbox import Client as asteriskClient from asterisk_mbox import Client as asteriskClient
self.hass = hass self.hass = hass
self.client = asteriskClient(host, port, password, self.handle_data) self.config = config
self.messages = [] self.messages = None
self.cdr = None
async_dispatcher_connect( dispatcher_connect(
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages) 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 @callback
def handle_data(self, command, msg): def handle_data(self, command, msg):
"""Handle changes to the mailbox.""" """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: 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( self.messages = sorted(
msg, key=lambda item: item['info']['origtime'], reverse=True) msg, key=lambda item: item['info']['origtime'], reverse=True)
async_dispatcher_send( if not isinstance(old_messages, list):
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages) 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 @callback
def _request_messages(self): def _request_messages(self):
"""Handle changes to the mailbox.""" """Handle changes to the mailbox."""
_LOGGER.debug("Requesting message list") _LOGGER.debug("Requesting message list")
self.client.messages() 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": { "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": { "totp": {
"error": { "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": { "step": {
"init": { "init": {

View File

@ -1,8 +1,26 @@
{ {
"mfa_setup": { "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": { "totp": {
"error": { "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": { "step": {
"init": { "init": {

View File

@ -1,5 +1,24 @@
{ {
"mfa_setup": { "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": { "totp": {
"error": { "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." "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": { "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": { "totp": {
"error": { "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": { "step": {
"init": { "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": { "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": { "totp": {
"error": { "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." "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": { "step": {
"init": { "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" "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
} }
}, },

View File

@ -1,5 +1,24 @@
{ {
"mfa_setup": { "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": { "totp": {
"error": { "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." "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": { "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": { "totp": {
"error": { "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." "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": { "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": { "totp": {
"error": { "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." "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": { "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": { "totp": {
"error": { "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." "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": { "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": { "totp": {
"error": { "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." "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": { "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": { "totp": {
"error": { "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" "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": { "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": { "totp": {
"error": { "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" "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. Exchange the authorization code retrieved from the login flow for tokens.
{ {
"client_id": "https://hassbian.local:8123/",
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": "411ee2f916e648d691e937ae9344681e" "code": "411ee2f916e648d691e937ae9344681e"
} }
@ -32,6 +33,7 @@ token.
Request a new access token using a refresh token. Request a new access token using a refresh token.
{ {
"client_id": "https://hassbian.local:8123/",
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": "IJKLMNOPQRST" "refresh_token": "IJKLMNOPQRST"
} }
@ -55,6 +57,66 @@ ever been granted by that refresh token. Response code will ALWAYS be 200.
"action": "revoke" "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 logging
import uuid import uuid
@ -63,8 +125,10 @@ from datetime import timedelta
from aiohttp import web from aiohttp import web
import voluptuous as vol 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 import websocket_api
from homeassistant.components.http import KEY_REAL_IP
from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView 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, 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_CREDENTIALS = 'credentials'
RESULT_TYPE_USER = 'user' RESULT_TYPE_USER = 'user'
@ -100,6 +186,21 @@ async def async_setup(hass, config):
WS_TYPE_CURRENT_USER, websocket_current_user, WS_TYPE_CURRENT_USER, websocket_current_user,
SCHEMA_WS_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 login_flow.async_setup(hass, store_result)
await mfa_setup_flow.async_setup(hass) await mfa_setup_flow.async_setup(hass)
@ -135,10 +236,12 @@ class TokenView(HomeAssistantView):
return await self._async_handle_revoke_token(hass, data) return await self._async_handle_revoke_token(hass, data)
if grant_type == 'authorization_code': 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': 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({ return self.json({
'error': 'unsupported_grant_type', 'error': 'unsupported_grant_type',
@ -163,7 +266,7 @@ class TokenView(HomeAssistantView):
await hass.auth.async_remove_refresh_token(refresh_token) await hass.auth.async_remove_refresh_token(refresh_token)
return web.Response(status=200) 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.""" """Handle authorization code request."""
client_id = data.get('client_id') client_id = data.get('client_id')
if client_id is None or not indieauth.verify_client_id(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, refresh_token = await hass.auth.async_create_refresh_token(user,
client_id) 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({ return self.json({
'access_token': access_token, 'access_token': access_token,
@ -209,7 +313,7 @@ class TokenView(HomeAssistantView):
int(refresh_token.access_token_expiration.total_seconds()), 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.""" """Handle authorization code request."""
client_id = data.get('client_id') client_id = data.get('client_id')
if client_id is not None and not indieauth.verify_client_id(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', 'error': 'invalid_request',
}, status_code=400) }, 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({ return self.json({
'access_token': access_token, 'access_token': access_token,
@ -343,3 +448,68 @@ def websocket_current_user(
})) }))
hass.async_create_task(async_get_current_user(connection.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.""" """Helpers to resolve client ID/secret."""
import asyncio import asyncio
from ipaddress import ip_address
from html.parser import HTMLParser from html.parser import HTMLParser
from ipaddress import ip_address, ip_network
from urllib.parse import urlparse, urljoin from urllib.parse import urlparse, urljoin
import aiohttp import aiohttp
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces from homeassistant.util.network import is_local
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'),
)
async def verify_redirect_uri(hass, client_id, redirect_uri): async def verify_redirect_uri(hass, client_id, redirect_uri):
@ -185,9 +174,7 @@ def _parse_client_id(client_id):
# Not an ip address # Not an ip address
pass pass
if (address is None or if address is None or is_local(address):
address in ALLOWED_IPS or
any(address in network for network in ALLOWED_NETWORKS)):
return parts return parts
raise ValueError('Hostname should be a domain name or local IP address') 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 "version": 1
} }
""" """
import aiohttp.web from aiohttp import web
import voluptuous as vol import voluptuous as vol
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
@ -95,11 +95,20 @@ class AuthProvidersView(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""Get available auth providers.""" """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([{ return self.json([{
'name': provider.name, 'name': provider.name,
'id': provider.id, 'id': provider.id,
'type': provider.type, 'type': provider.type,
} for provider in request.app['hass'].auth.auth_providers]) } for provider in hass.auth.auth_providers])
def _prepare_result_json(result): def _prepare_result_json(result):
@ -139,7 +148,7 @@ class LoginFlowIndexView(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""Do not allow index of flows in progress.""" """Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405) return web.Response(status=405)
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('client_id'): str, vol.Required('client_id'): str,
@ -217,8 +226,9 @@ class LoginFlowResourceView(HomeAssistantView):
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
# @log_invalid_auth does not work here since it returns HTTP 200 # @log_invalid_auth does not work here since it returns HTTP 200
# need manually log failed login attempts # need manually log failed login attempts
if result['errors'] is not None and \ if (result.get('errors') is not None and
result['errors'].get('base') == 'invalid_auth': result['errors'].get('base') in ['invalid_auth',
'invalid_code']):
await process_wrong_login(request) await process_wrong_login(request)
return self.json(_prepare_result_json(result)) return self.json(_prepare_result_json(result))

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@ def async_trigger(hass, config, action):
@callback @callback
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
hass.async_run_job(action, { hass.async_run_job(action({
'trigger': { 'trigger': {
'platform': 'state', 'platform': 'state',
'entity_id': entity, 'entity_id': entity,
@ -51,7 +51,7 @@ def async_trigger(hass, config, action):
'to_state': to_s, 'to_state': to_s,
'for': time_delta, 'for': time_delta,
} }
}) }, context=to_s.context))
# Ignore changes to state attributes if from/to is in use # 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 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 @callback
def template_listener(entity_id, from_s, to_s): def template_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
hass.async_run_job(action, { hass.async_run_job(action({
'trigger': { 'trigger': {
'platform': 'template', 'platform': 'template',
'entity_id': entity_id, 'entity_id': entity_id,
'from_state': from_s, 'from_state': from_s,
'to_state': to_s, 'to_state': to_s,
}, },
}) }, context=to_s.context))
return async_track_template(hass, value_template, template_listener) 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 # pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \ if event == EVENT_ENTER and not from_match and to_match or \
event == EVENT_LEAVE and from_match and not to_match: event == EVENT_LEAVE and from_match and not to_match:
hass.async_run_job(action, { hass.async_run_job(action({
'trigger': { 'trigger': {
'platform': 'zone', 'platform': 'zone',
'entity_id': entity, 'entity_id': entity,
@ -60,7 +60,7 @@ def async_trigger(hass, config, action):
'zone': zone_state, 'zone': zone_state,
'event': event, 'event': event,
}, },
}) }, context=to_s.context))
return async_track_state_change(hass, entity_id, zone_automation_listener, return async_track_state_change(hass, entity_id, zone_automation_listener,
MATCH_ALL, MATCH_ALL) MATCH_ALL, MATCH_ALL)

View File

@ -124,7 +124,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
if not check_control_messages: if not check_control_messages:
result['check_control_messages'] = 'OK' result['check_control_messages'] = 'OK'
else: 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': elif self._attribute == 'charging_status':
result['charging_status'] = vehicle_state.charging_status.value result['charging_status'] = vehicle_state.charging_status.value
# pylint: disable=protected-access # pylint: disable=protected-access

View File

@ -127,6 +127,7 @@ class DeconzBinarySensor(BinarySensorDevice):
self._sensor.uniqueid.count(':') != 7): self._sensor.uniqueid.count(':') != 7):
return None return None
serial = self._sensor.uniqueid.split('-', 1)[0] serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},
@ -134,4 +135,5 @@ class DeconzBinarySensor(BinarySensorDevice):
'model': self._sensor.modelid, 'model': self._sensor.modelid,
'name': self._sensor.name, 'name': self._sensor.name,
'sw_version': self._sensor.swversion, '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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP Cloud binary sensor from a config entry.""" """Set up the HomematicIP Cloud binary sensor from a config entry."""
from homematicip.aio.device import ( from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector) AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector,
AsyncWaterSensor, AsyncRotaryHandleSensor)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = [] devices = []
for device in home.devices: for device in home.devices:
if isinstance(device, AsyncShutterContact): if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)):
devices.append(HomematicipShutterContact(home, device)) devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, AsyncMotionDetectorIndoor): elif isinstance(device, AsyncMotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device)) devices.append(HomematicipMotionDetector(home, device))
elif isinstance(device, AsyncSmokeDetector): elif isinstance(device, AsyncSmokeDetector):
devices.append(HomematicipSmokeDetector(home, device)) devices.append(HomematicipSmokeDetector(home, device))
elif isinstance(device, AsyncWaterSensor):
devices.append(HomematicipWaterDetector(home, device))
if devices: if devices:
async_add_entities(devices) async_add_entities(devices)
@ -91,3 +94,17 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
def is_on(self): def is_on(self):
"""Return true if smoke is detected.""" """Return true if smoke is detected."""
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF 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 import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components import mqtt from homeassistant.components import mqtt, binary_sensor
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASSES_SCHEMA) BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC,
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) 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 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__) _LOGGER = logging.getLogger(__name__)
@ -44,13 +48,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
def async_setup_platform(hass, config, async_add_entities, async_add_entities, discovery_info=None):
discovery_info=None): """Set up MQTT binary sensor through configuration.yaml."""
"""Set up the MQTT binary sensor.""" await _async_setup_entity(hass, config, async_add_entities)
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
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) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
@ -68,19 +87,22 @@ def async_setup_platform(hass, config, async_add_entities,
config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE),
value_template, value_template,
config.get(CONF_UNIQUE_ID), 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.""" """Representation a binary sensor that is updated by MQTT."""
def __init__(self, name, state_topic, availability_topic, device_class, def __init__(self, name, state_topic, availability_topic, device_class,
qos, force_update, payload_on, payload_off, payload_available, qos, force_update, payload_on, payload_off, payload_available,
payload_not_available, value_template, payload_not_available, value_template,
unique_id: Optional[str]): unique_id: Optional[str], discovery_hash):
"""Initialize the MQTT binary sensor.""" """Initialize the MQTT binary sensor."""
super().__init__(availability_topic, qos, payload_available, MqttAvailability.__init__(self, availability_topic, qos,
payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
self._name = name self._name = name
self._state = None self._state = None
self._state_topic = state_topic self._state_topic = state_topic
@ -91,11 +113,13 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
self._force_update = force_update self._force_update = force_update
self._template = value_template self._template = value_template
self._unique_id = unique_id self._unique_id = unique_id
self._discovery_hash = discovery_hash
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Subscribe mqtt events.""" """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 @callback
def state_message_received(topic, payload, qos): def state_message_received(topic, payload, qos):

View File

@ -147,6 +147,11 @@ class NestActivityZoneSensor(NestBinarySensor):
self.zone = zone self.zone = zone
self._name = "{} {} activity".format(self._name, self.zone.name) 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 @property
def device_class(self): def device_class(self):
"""Return the device class of the binary sensor.""" """Return the device class of the binary sensor."""

View File

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

View File

@ -92,6 +92,11 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
"""Return the name of this sensor including the controller name.""" """Return the name of this sensor including the controller name."""
return "{} online".format(self._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 @property
def device_class(self) -> str: def device_class(self) -> str:
"""Return the class of this device, from component DEVICE_CLASSES.""" """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, CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS) HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
_LOGGER = logging.getLogger(__name__) _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 = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update() rest.update()
if rest.data is None: if rest.data is None:
_LOGGER.error("Unable to fetch REST data from %s", resource) raise PlatformNotReady
return False
# 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( add_entities([RestBinarySensor(
hass, rest, name, device_class, value_template)], True) hass, rest, name, device_class, value_template)])
class RestBinarySensor(BinarySensorDevice): class RestBinarySensor(BinarySensorDevice):

View File

@ -44,14 +44,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
ring = hass.data[DATA_RING] ring = hass.data[DATA_RING]
sensors = [] sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS): for device in ring.doorbells: # ring.doorbells is doing I/O
for device in ring.doorbells: for sensor_type in config[CONF_MONITORED_CONDITIONS]:
if 'doorbell' in SENSOR_TYPES[sensor_type][1]: if 'doorbell' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass, sensors.append(RingBinarySensor(hass,
device, device,
sensor_type)) 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]: if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass, sensors.append(RingBinarySensor(hass,
device, 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.spc/ https://home-assistant.io/components/binary_sensor.spc/
""" """
import asyncio
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.spc import ATTR_DISCOVER_DEVICES, DATA_REGISTRY from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import callback
from homeassistant.components.spc import (
ATTR_DISCOVER_DEVICES, SIGNAL_UPDATE_SENSOR)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SPC_TYPE_TO_DEVICE_CLASS = {
'0': 'motion',
'1': 'opening',
'3': 'smoke',
}
SPC_INPUT_TO_SENSOR_STATE = { def _get_device_class(zone_type):
'0': STATE_OFF, from pyspcwebgw.const import ZoneType
'1': STATE_ON, return {
} ZoneType.ALARM: 'motion',
ZoneType.ENTRY_EXIT: 'opening',
ZoneType.FIRE: 'smoke',
}.get(zone_type)
def _get_device_class(spc_type): async def async_setup_platform(hass, config, async_add_entities,
"""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): discovery_info=None):
"""Set up the SPC binary sensor.""" """Set up the SPC binary sensor."""
if (discovery_info is None or if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None): discovery_info[ATTR_DISCOVER_DEVICES] is None):
return return
async_add_entities( async_add_entities(SpcBinarySensor(zone)
_create_sensor(hass, zone)
for zone in discovery_info[ATTR_DISCOVER_DEVICES] for zone in discovery_info[ATTR_DISCOVER_DEVICES]
if _get_device_class(zone['type'])) if _get_device_class(zone.type))
class SpcBinarySensor(BinarySensorDevice): class SpcBinarySensor(BinarySensorDevice):
"""Representation of a sensor based on a SPC zone.""" """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.""" """Initialize the sensor device."""
self._zone_id = zone_id self._zone = zone
self._name = name
self._state = state
self._device_class = device_class
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 @callback
def async_update_from_spc(self, state, extra): def _update_callback(self):
"""Update the state of the device.""" """Call update method."""
self._state = state self.async_schedule_update_ha_state(True)
yield from self.async_update_ha_state()
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._zone.name
@property @property
def is_on(self): def is_on(self):
"""Whether the device is switched on.""" """Whether the device is switched on."""
return self._state == STATE_ON from pyspcwebgw.const import ZoneInput
return self._zone.input == ZoneInput.OPEN
@property @property
def hidden(self) -> bool: def hidden(self) -> bool:
@ -100,4 +79,4 @@ class SpcBinarySensor(BinarySensorDevice):
@property @property
def device_class(self): def device_class(self):
"""Return the device class.""" """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) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.wirelesstag import ( from homeassistant.components.wirelesstag import (
DOMAIN as WIRELESSTAG_DOMAIN, DOMAIN as WIRELESSTAG_DOMAIN,
WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
WIRELESSTAG_TYPE_ALSPRO,
WIRELESSTAG_TYPE_WEMO_DEVICE,
SIGNAL_BINARY_EVENT_UPDATE, SIGNAL_BINARY_EVENT_UPDATE,
WirelessTagBaseSensor) WirelessTagBaseSensor)
from homeassistant.const import ( from homeassistant.const import (
@ -30,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
# On means in range, Off means out of range # On means in range, Off means out of range
SENSOR_PRESENCE = 'presence' SENSOR_PRESENCE = 'presence'
# On means motion detected, Off means cear # On means motion detected, Off means clear
SENSOR_MOTION = 'motion' SENSOR_MOTION = 'motion'
# On means open, Off means closed # On means open, Off means closed
@ -55,49 +52,21 @@ SENSOR_LIGHT = 'light'
SENSOR_MOISTURE = 'moisture' SENSOR_MOISTURE = 'moisture'
# On means tag battery is low, Off means normal # 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', # Sensor types: Name, device_class, push notification type representing 'on',
# attr to check # attr to check
SENSOR_TYPES = { SENSOR_TYPES = {
SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', { SENSOR_PRESENCE: 'Presence',
"on": "oor", SENSOR_MOTION: 'Motion',
"off": "back_in_range" SENSOR_DOOR: 'Door',
}, 2], SENSOR_COLD: 'Cold',
SENSOR_MOTION: ['Motion', 'motion', 'is_moved', { SENSOR_HEAT: 'Heat',
"on": "motion_detected", SENSOR_DRY: 'Too dry',
}, 5], SENSOR_WET: 'Too wet',
SENSOR_DOOR: ['Door', 'door', 'is_door_open', { SENSOR_LIGHT: 'Light',
"on": "door_opened", SENSOR_MOISTURE: 'Leak',
"off": "door_closed" SENSOR_BATTERY: 'Low Battery'
}, 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]
} }
@ -114,7 +83,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sensors = [] sensors = []
tags = platform.tags tags = platform.tags
for tag in tags.values(): 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): for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type in allowed_sensor_types: if sensor_type in allowed_sensor_types:
sensors.append(WirelessTagBinarySensor(platform, tag, sensors.append(WirelessTagBinarySensor(platform, tag,
@ -127,59 +96,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
"""A binary sensor implementation for WirelessTags.""" """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): def __init__(self, api, tag, sensor_type):
"""Initialize a binary sensor for a Wireless Sensor Tags.""" """Initialize a binary sensor for a Wireless Sensor Tags."""
super().__init__(api, tag) super().__init__(api, tag)
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._name = '{0} {1}'.format(self._tag.name, self._name = '{0} {1}'.format(self._tag.name,
SENSOR_TYPES[self._sensor_type][0]) self.event.human_readable_name)
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]
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
tag_id = self.tag_id tag_id = self.tag_id
event_type = self.device_class event_type = self.device_class
mac = self.tag_manager_mac
async_dispatcher_connect( async_dispatcher_connect(
self.hass, 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) self._on_binary_event_callback)
@property @property
@ -190,7 +121,12 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
@property @property
def device_class(self): def device_class(self):
"""Return the class of the binary sensor.""" """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 @property
def principal_value(self): def principal_value(self):
@ -198,9 +134,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
Subclasses need override based on type of sensor. Subclasses need override based on type of sensor.
""" """
return ( return STATE_ON if self.event.is_state_on else STATE_OFF
STATE_ON if getattr(self._tag, self._tag_attr, False)
else STATE_OFF)
def updated_state_value(self): def updated_state_value(self):
"""Use raw princial value.""" """Use raw princial value."""
@ -208,7 +142,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
@callback @callback
def _on_binary_event_callback(self, event): 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' # state should be 'on' or 'off'
self._state = event.data.get('state') self._state = event.data.get('state')
self.async_schedule_update_ha_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 from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.helpers.config_validation as cv 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 # List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime # There seems to be no way to get the list out at runtime
ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', ALL_COUNTRIES = [
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY'
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ',
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland', 'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', 'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', 'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH',
'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
'UnitedStates', 'US', 'Wales'] ]
ALLOWED_DAYS = WEEKDAYS + ['holiday']
CONF_COUNTRY = 'country' CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province' CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays' CONF_WORKDAYS = 'workdays'
CONF_EXCLUDES = 'excludes'
CONF_OFFSET = 'days_offset'
# By default, Monday - Friday are workdays # By default, Monday - Friday are workdays
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri'] DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
CONF_EXCLUDES = 'excludes'
# By default, public holidays, Saturdays and Sundays are excluded from workdays # By default, public holidays, Saturdays and Sundays are excluded from workdays
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday'] DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
DEFAULT_NAME = 'Workday Sensor' DEFAULT_NAME = 'Workday Sensor'
ALLOWED_DAYS = WEEKDAYS + ['holiday']
CONF_OFFSET = 'days_offset'
DEFAULT_OFFSET = 0 DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -86,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else: else:
_LOGGER.error("There is no province/state %s in country %s", _LOGGER.error("There is no province/state %s in country %s",
province, country) province, country)
return False return
_LOGGER.debug("Found the following holidays for your configuration:") _LOGGER.debug("Found the following holidays for your configuration:")
for date, name in sorted(obj_holidays.items()): 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, async def _async_setup_remote(hass, config, async_add_entities,
discovery_info): discovery_info):
async def safe(coro): remote = Remote(**discovery_info)
"""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)
if discovery_info['new_join']: if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters'] out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters: if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id] cluster = out_clusters[OnOff.cluster_id]
await safe(cluster.bind()) await zha.configure_reporting(
await safe(cluster.configure_reporting(0, 0, 600, 1)) remote.entity_id, cluster, 0, min_report=0, max_report=600,
reportable_change=1
)
if LevelControl.cluster_id in out_clusters: if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id] cluster = out_clusters[LevelControl.cluster_id]
await safe(cluster.bind()) await zha.configure_reporting(
await safe(cluster.configure_reporting(0, 1, 600, 1)) remote.entity_id, cluster, 0, min_report=1, max_report=600,
reportable_change=1
)
sensor = Switch(**discovery_info) async_add_entities([remote], update_before_add=True)
async_add_entities([sensor], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice): class BinarySensor(zha.Entity, BinarySensorDevice):
@ -131,17 +128,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
async def async_update(self): async def async_update(self):
"""Retrieve latest state.""" """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, result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status'], ['zone_status'],
allow_cache=False) allow_cache=False,
only_cache=(not self._initialized))
state = result.get('zone_status', self._state) state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)): if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3 self._state = result.get('zone_status', self._state) & 3
class Switch(zha.Entity, BinarySensorDevice): class Remote(zha.Entity, BinarySensorDevice):
"""ZHA switch/remote controller/button.""" """ZHA switch/remote controller/button."""
_domain = DOMAIN _domain = DOMAIN

View File

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

View File

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

View File

@ -83,65 +83,71 @@ class Image:
content = attr.ib(type=bytes) 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 @bind_hass
async def async_get_image(hass, entity_id, timeout=10): async def async_get_image(hass, entity_id, timeout=10):
"""Fetch an image from a camera entity.""" """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) component = hass.data.get(DOMAIN)
if component is None: if component is None:
@ -155,14 +161,7 @@ async def async_get_image(hass, entity_id, timeout=10):
if not camera.is_on: if not camera.is_on:
raise HomeAssistantError('Camera is off') raise HomeAssistantError('Camera is off')
with suppress(asyncio.CancelledError, asyncio.TimeoutError): return camera
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')
async def async_setup(hass, config): async def async_setup(hass, config):
@ -290,39 +289,8 @@ class Camera(Entity):
This method must be run in the event loop. This method must be run in the event loop.
""" """
response = web.StreamResponse() return await async_get_still_stream(request, self.async_camera_image,
response.content_type = ('multipart/x-mixed-replace; ' self.content_type, interval)
'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
async def handle_async_mjpeg_stream(self, request): async def handle_async_mjpeg_stream(self, request):
"""Serve an HTTP MJPEG stream from the camera. """Serve an HTTP MJPEG stream from the camera.

View File

@ -50,7 +50,7 @@ class AxisCamera(MjpegCamera):
def __init__(self, hass, config, port): def __init__(self, hass, config, port):
"""Initialize Axis Communications camera component.""" """Initialize Axis Communications camera component."""
super().__init__(hass, config) super().__init__(config)
self.port = port self.port = port
dispatcher_connect( dispatcher_connect(
hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip) 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.""" """Set up a MJPEG IP Camera."""
if discovery_info: if discovery_info:
config = PLATFORM_SCHEMA(discovery_info) config = PLATFORM_SCHEMA(discovery_info)
async_add_entities([MjpegCamera(hass, config)]) async_add_entities([MjpegCamera(config)])
def extract_image_from_mjpeg(stream): def extract_image_from_mjpeg(stream):
@ -65,7 +65,7 @@ def extract_image_from_mjpeg(stream):
class MjpegCamera(Camera): class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL.""" """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.""" """Initialize a MJPEG camera."""
super().__init__() super().__init__()
self._name = device_info.get(CONF_NAME) self._name = device_info.get(CONF_NAME)

View File

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

View File

@ -62,6 +62,23 @@ class NestCamera(Camera):
"""Return the name of the nest, if any.""" """Return the name of the nest, if any."""
return self._name 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 @property
def should_poll(self): def should_poll(self):
"""Nest camera should poll periodically.""" """Nest camera should poll periodically."""

View File

@ -7,17 +7,15 @@ https://www.home-assistant.io/components/camera.proxy/
import asyncio import asyncio
import logging import logging
import aiohttp
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH 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 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 from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import async_get_still_stream
REQUIREMENTS = ['pillow==5.2.0'] REQUIREMENTS = ['pillow==5.2.0']
@ -158,22 +156,14 @@ class ProxyCamera(Camera):
return self._last_image return self._last_image
self._last_image_time = now self._last_image_time = now
url = "{}/api/camera_proxy/{}".format( image = await self.hass.components.camera.async_get_image(
self.hass.config.api.base_url, self._proxied_camera) self._proxied_camera)
try: if not image:
websession = async_get_clientsession(self.hass) _LOGGER.error("Error getting original camera image")
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)
return self._last_image return self._last_image
image = await self.hass.async_add_job( image = await self.hass.async_add_job(
_resize_image, image, self._image_opts) _resize_image, image.content, self._image_opts)
if self._cache_images: if self._cache_images:
self._last_image = image self._last_image = image
@ -181,56 +171,28 @@ class ProxyCamera(Camera):
async def handle_async_mjpeg_stream(self, request): async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images.""" """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: if not self._stream_opts:
return await async_aiohttp_proxy_web( return await self.hass.components.camera.async_get_mjpeg_stream(
self.hass, request, stream_coro) request, self._proxied_camera)
response = aiohttp.web.StreamResponse() return await async_get_still_stream(
response.content_type = ( request, self._async_stream_image,
'multipart/x-mixed-replace; boundary=--frameboundary') self.content_type, self.frame_interval)
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
@property @property
def name(self): def name(self):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name 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,\ from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING STATE_IDLE, STATE_RECORDING
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.view import KEY_AUTHENTICATED,\
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST 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 import config_validation as cv
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -25,11 +27,13 @@ DEPENDENCIES = ['http']
CONF_BUFFER_SIZE = 'buffer' CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field' CONF_IMAGE_FIELD = 'field'
CONF_TOKEN = 'token'
DEFAULT_NAME = "Push Camera" DEFAULT_NAME = "Push Camera"
ATTR_FILENAME = 'filename' ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip' ATTR_LAST_TRIP = 'last_trip'
ATTR_TOKEN = 'token'
PUSH_CAMERA_DATA = 'push_camera' PUSH_CAMERA_DATA = 'push_camera'
@ -39,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All( vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
cv.time_period, cv.positive_timedelta), cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string, 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], cameras = [PushCamera(config[CONF_NAME],
config[CONF_BUFFER_SIZE], config[CONF_BUFFER_SIZE],
config[CONF_TIMEOUT])] config[CONF_TIMEOUT],
config.get(CONF_TOKEN))]
hass.http.register_view(CameraPushReceiver(hass, hass.http.register_view(CameraPushReceiver(hass,
config[CONF_IMAGE_FIELD])) config[CONF_IMAGE_FIELD]))
@ -63,6 +69,7 @@ class CameraPushReceiver(HomeAssistantView):
url = "/api/camera_push/{entity_id}" url = "/api/camera_push/{entity_id}"
name = 'api:camera_push:camera_entity' name = 'api:camera_push:camera_entity'
requires_auth = False
def __init__(self, hass, image_field): def __init__(self, hass, image_field):
"""Initialize CameraPushReceiver with camera entity.""" """Initialize CameraPushReceiver with camera entity."""
@ -75,8 +82,21 @@ class CameraPushReceiver(HomeAssistantView):
if _camera is None: if _camera is None:
_LOGGER.error("Unknown %s", entity_id) _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), 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: try:
data = await request.post() data = await request.post()
@ -95,7 +115,7 @@ class CameraPushReceiver(HomeAssistantView):
class PushCamera(Camera): class PushCamera(Camera):
"""The representation of a Push 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.""" """Initialize push camera component."""
super().__init__() super().__init__()
self._name = name self._name = name
@ -106,6 +126,7 @@ class PushCamera(Camera):
self._timeout = timeout self._timeout = timeout
self.queue = deque([], buffer_size) self.queue = deque([], buffer_size)
self._current_image = None self._current_image = None
self.token = token
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Call when entity is added to hass.""" """Call when entity is added to hass."""
@ -168,5 +189,6 @@ class PushCamera(Camera):
name: value for name, value in ( name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip), (ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename), (ATTR_FILENAME, self._filename),
(ATTR_TOKEN, self.token),
) if value is not None ) if value is not None
} }

View File

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

View File

@ -63,3 +63,39 @@ onvif_ptz:
zoom: zoom:
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
example: "ZOOM_IN" 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.zoneminder/ https://home-assistant.io/components/camera.zoneminder/
""" """
import asyncio
import logging import logging
from urllib.parse import urljoin, urlencode
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.components.camera.mjpeg import ( from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN
from homeassistant.components import zoneminder
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zoneminder'] DEPENDENCIES = ['zoneminder']
DOMAIN = 'zoneminder'
# From ZoneMinder's web/includes/config.php.in
ZM_STATE_ALARM = "2"
def _get_image_url(hass, monitor, mode): def setup_platform(hass, config, add_entities, discovery_info=None):
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):
"""Set up the ZoneMinder cameras.""" """Set up the ZoneMinder cameras."""
cameras = [] zm_client = hass.data[ZONEMINDER_DOMAIN]
monitors = zoneminder.get_state('api/monitors.json')
monitors = zm_client.get_monitors()
if not monitors: if not monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder") _LOGGER.warning("Could not fetch monitors from ZoneMinder")
return return
for i in monitors['monitors']: cameras = []
monitor = i['Monitor'] for monitor in monitors:
_LOGGER.info("Initializing camera %s", monitor.id)
if monitor['Function'] == 'None': cameras.append(ZoneMinderCamera(monitor))
_LOGGER.info("Skipping camera %s", monitor['Id']) add_entities(cameras)
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)
class ZoneMinderCamera(MjpegCamera): class ZoneMinderCamera(MjpegCamera):
"""Representation of a ZoneMinder Monitor Stream.""" """Representation of a ZoneMinder Monitor Stream."""
def __init__(self, hass, device_info, monitor): def __init__(self, monitor):
"""Initialize as a subclass of MjpegCamera.""" """Initialize as a subclass of MjpegCamera."""
super().__init__(hass, device_info) device_info = {
self._monitor_id = int(monitor['Id']) 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._is_recording = None
self._monitor = monitor
@property @property
def should_poll(self): def should_poll(self):
@ -97,17 +53,8 @@ class ZoneMinderCamera(MjpegCamera):
def update(self): def update(self):
"""Update our recording state from the ZM API.""" """Update our recording state from the ZM API."""
_LOGGER.debug("Updating camera state for monitor %i", self._monitor_id) _LOGGER.debug("Updating camera state for monitor %i", self._monitor.id)
status_response = zoneminder.get_state( self._is_recording = self._monitor.is_recording
'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
@property @property
def is_recording(self): def is_recording(self):

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.", "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": { "step": {
"confirm": { "confirm": {

View File

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

View File

@ -2,12 +2,14 @@
"config": { "config": {
"abort": { "abort": {
"no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.", "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": { "step": {
"confirm": { "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( 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 import voluptuous as vol
from homeassistant.loader import bind_hass
from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.temperature import display_temp as show_temp
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
@ -20,7 +19,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
PRECISION_TENTHS, ) PRECISION_TENTHS)
DEFAULT_MIN_TEMP = 7 DEFAULT_MIN_TEMP = 7
DEFAULT_MAX_TEMP = 35 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): async def async_setup(hass, config):
"""Set up climate devices.""" """Set up climate devices."""
component = hass.data[DOMAIN] = \ component = hass.data[DOMAIN] = \

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
TEMP_FAHRENHEIT) TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyeconet==0.0.5'] REQUIREMENTS = ['pyeconet==0.0.6']
_LOGGER = logging.getLogger(__name__) _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 # Ensure we update the current operation after changing the mode
self.schedule_update_ha_state() 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): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)

View File

@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE, CONF_REGION) 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__) _LOGGER = logging.getLogger(__name__)

View File

@ -10,7 +10,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components import mqtt from homeassistant.components import mqtt, climate
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
@ -21,9 +21,13 @@ from homeassistant.components.climate import (
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE) STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN,
CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability) 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 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, from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH) SPEED_HIGH)
@ -126,13 +130,28 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@asyncio.coroutine async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
def async_setup_platform(hass, config, async_add_entities, async_add_entities, discovery_info=None):
discovery_info=None): """Set up MQTT climate device through configuration.yaml."""
"""Set up the MQTT climate devices.""" await _async_setup_entity(hass, config, async_add_entities)
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
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 = ( template_keys = (
CONF_POWER_STATE_TEMPLATE, CONF_POWER_STATE_TEMPLATE,
CONF_MODE_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_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE),
config.get(CONF_MIN_TEMP), 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.""" """Representation of an MQTT climate device."""
def __init__(self, hass, name, topic, value_templates, qos, retain, 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, current_swing_mode, current_operation, aux, send_if_off,
payload_on, payload_off, availability_topic, payload_on, payload_off, availability_topic,
payload_available, payload_not_available, payload_available, payload_not_available,
min_temp, max_temp): min_temp, max_temp, discovery_hash):
"""Initialize the climate device.""" """Initialize the climate device."""
super().__init__(availability_topic, qos, payload_available, MqttAvailability.__init__(self, availability_topic, qos,
payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
self.hass = hass self.hass = hass
self._name = name self._name = name
self._topic = topic self._topic = topic
@ -235,11 +256,13 @@ class MqttClimate(MqttAvailability, ClimateDevice):
self._payload_off = payload_off self._payload_off = payload_off
self._min_temp = min_temp self._min_temp = min_temp
self._max_temp = max_temp self._max_temp = max_temp
self._discovery_hash = discovery_hash
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Handle being added to home assistant.""" """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 @callback
def handle_current_temp_received(topic, payload, qos): 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): def device_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
return { return {
ATTR_FAN: self._fmode, ATTR_FAN: self._fstate,
ATTR_MODE: self._tmode, ATTR_MODE: self._tstate,
} }
@property @property

View File

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

View File

@ -10,8 +10,8 @@ from homeassistant.components.climate import (
DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import ZWaveDeviceEntity, async_setup_platform)
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) 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 helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c 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 from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.6.1'] 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__) _LOGGER = logging.getLogger(__name__)
_UNDEF = object()
CONF_ALEXA = 'alexa' CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases' CONF_ALIASES = 'aliases'
@ -39,6 +44,7 @@ CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_RELAYER = 'relayer' CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id' CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url' CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
DEFAULT_MODE = 'production' DEFAULT_MODE = 'production'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -79,6 +85,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_REGION): str, vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): 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_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}), }),
@ -114,18 +121,21 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions, def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None, 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.""" """Create an instance of Cloud."""
self.hass = hass self.hass = hass
self.mode = mode self.mode = mode
self.alexa_config = alexa self.alexa_config = alexa
self._google_actions = google_actions self._google_actions = google_actions
self._gactions_config = None self._gactions_config = None
self._prefs = None
self.jwt_keyset = None self.jwt_keyset = None
self.id_token = None self.id_token = None
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None
self.iot = iot.CloudIoT(self) self.iot = iot.CloudIoT(self)
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
if mode == MODE_DEV: if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id self.cognito_client_id = cognito_client_id
@ -133,6 +143,7 @@ class Cloud:
self.region = region self.region = region
self.relayer = relayer self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url self.google_actions_sync_url = google_actions_sync_url
self.subscription_info_url = subscription_info_url
else: else:
info = SERVERS[mode] info = SERVERS[mode]
@ -142,6 +153,7 @@ class Cloud:
self.region = info['region'] self.region = info['region']
self.relayer = info['relayer'] self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url'] self.google_actions_sync_url = info['google_actions_sync_url']
self.subscription_info_url = info['subscription_info_url']
@property @property
def is_logged_in(self): def is_logged_in(self):
@ -188,6 +200,16 @@ class Cloud:
return self._gactions_config 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): def path(self, *parts):
"""Get config path inside cloud dir. """Get config path inside cloud dir.
@ -195,6 +217,15 @@ class Cloud:
""" """
return self.hass.config.path(CONFIG_DIR, *parts) 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 @asyncio.coroutine
def logout(self): def logout(self):
"""Close connection and remove all credentials.""" """Close connection and remove all credentials."""
@ -217,10 +248,23 @@ class Cloud:
'refresh_token': self.refresh_token, 'refresh_token': self.refresh_token,
}, indent=4)) }, indent=4))
@asyncio.coroutine async def async_start(self, _):
def async_start(self, _):
"""Start the cloud component.""" """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. # Fetching keyset can fail if internet is not up yet.
if not success: if not success:
@ -241,7 +285,7 @@ class Cloud:
with open(user_info, 'rt') as file: with open(user_info, 'rt') as file:
return json.loads(file.read()) 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: if info is None:
return return
@ -260,6 +304,15 @@ class Cloud:
self.hass.add_job(self.iot.connect()) 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 @asyncio.coroutine
def _fetch_jwt_keyset(self): def _fetch_jwt_keyset(self):
"""Fetch the JWT keyset for the Cognito instance.""" """Fetch the JWT keyset for the Cognito instance."""

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