diff --git a/.coveragerc b/.coveragerc index c3c6f419d17..71c0acecf84 100644 --- a/.coveragerc +++ b/.coveragerc @@ -42,6 +42,7 @@ omit = homeassistant/components/asterisk_mbox.py homeassistant/components/*/asterisk_mbox.py + homeassistant/components/*/asterisk_cdr.py homeassistant/components/august.py homeassistant/components/*/august.py @@ -92,6 +93,9 @@ omit = homeassistant/components/ecobee.py homeassistant/components/*/ecobee.py + homeassistant/components/edp_redy.py + homeassistant/components/*/edp_redy.py + homeassistant/components/egardia.py homeassistant/components/*/egardia.py @@ -101,6 +105,9 @@ omit = homeassistant/components/envisalink.py homeassistant/components/*/envisalink.py + homeassistant/components/evohome.py + homeassistant/components/*/evohome.py + homeassistant/components/fritzbox.py homeassistant/components/switch/fritzbox.py @@ -123,7 +130,8 @@ omit = homeassistant/components/hangouts/const.py homeassistant/components/hangouts/hangouts_bot.py homeassistant/components/hangouts/hangups_utils.py - homeassistant/components/*/hangouts.py + homeassistant/components/hangouts/intents.py + homeassistant/components/*/hangouts.py homeassistant/components/hdmi_cec.py homeassistant/components/*/hdmi_cec.py @@ -140,17 +148,20 @@ omit = homeassistant/components/homematicip_cloud.py homeassistant/components/*/homematicip_cloud.py + homeassistant/components/huawei_lte.py + homeassistant/components/*/huawei_lte.py + homeassistant/components/hydrawise.py homeassistant/components/*/hydrawise.py homeassistant/components/ihc/* homeassistant/components/*/ihc.py - + homeassistant/components/insteon/* homeassistant/components/*/insteon.py homeassistant/components/insteon_local.py - + homeassistant/components/insteon_plm.py homeassistant/components/ios.py @@ -183,6 +194,9 @@ omit = homeassistant/components/linode.py homeassistant/components/*/linode.py + homeassistant/components/logi_circle.py + homeassistant/components/*/logi_circle.py + homeassistant/components/lutron.py homeassistant/components/*/lutron.py @@ -228,7 +242,7 @@ omit = homeassistant/components/opencv.py homeassistant/components/*/opencv.py - homeassistant/components/openuv.py + homeassistant/components/openuv/__init__.py homeassistant/components/*/openuv.py homeassistant/components/pilight.py @@ -377,6 +391,7 @@ omit = homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/totalconnect.py + homeassistant/components/alarm_control_panel/yale_smart_alarm.py homeassistant/components/apiai.py homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/concord232.py @@ -414,6 +429,7 @@ omit = homeassistant/components/climate/honeywell.py homeassistant/components/climate/knx.py homeassistant/components/climate/oem.py + homeassistant/components/climate/opentherm_gw.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py homeassistant/components/climate/sensibo.py @@ -497,6 +513,7 @@ omit = homeassistant/components/light/lw12wifi.py homeassistant/components/light/mystrom.py homeassistant/components/light/nanoleaf_aurora.py + homeassistant/components/light/opple.py homeassistant/components/light/osramlightify.py homeassistant/components/light/piglow.py homeassistant/components/light/rpi_gpio_pwm.py @@ -666,6 +683,7 @@ omit = homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/gearbest.py homeassistant/components/sensor/geizhals.py + homeassistant/components/sensor/gitlab_ci.py homeassistant/components/sensor/gitter.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/google_travel_time.py @@ -683,6 +701,7 @@ omit = homeassistant/components/sensor/kwb.py homeassistant/components/sensor/lacrosse.py homeassistant/components/sensor/lastfm.py + homeassistant/components/sensor/linky.py homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/luftdaten.py @@ -739,6 +758,7 @@ omit = homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/spotcrime.py + homeassistant/components/sensor/starlingbank.py homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/swiss_hydrological_data.py @@ -793,6 +813,7 @@ omit = homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py + homeassistant/components/switch/switchbot.py homeassistant/components/switch/switchmate.py homeassistant/components/switch/telnet.py homeassistant/components/switch/tplink.py @@ -810,6 +831,7 @@ omit = homeassistant/components/weather/bom.py homeassistant/components/weather/buienradar.py homeassistant/components/weather/darksky.py + homeassistant/components/weather/met.py homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py homeassistant/components/weather/zamg.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c2f65f9a8be..1e37cf86fc3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ **Related issue (if applicable):** fixes # -**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# +**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io# ## Example entry for `configuration.yaml` (if applicable): ```yaml @@ -15,7 +15,7 @@ - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** If user exposed functionality or configuration variables are added/changed: - - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) + - [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) If the code communicates with devices, web services, or third-party tools: - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). diff --git a/CODEOWNERS b/CODEOWNERS index 9015e0fb44c..a6362319985 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -50,6 +50,7 @@ homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills +homeassistant/components/device_tracker/huawei_router.py @abmantis homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/light/lifx.py @amelchio @@ -72,6 +73,7 @@ homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/filter.py @dgomes homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 +homeassistant/components/sensor/jewish_calendar.py @tsvi homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/nsw_fuel_station.py @nickw444 homeassistant/components/sensor/pollen.py @bachya @@ -91,11 +93,15 @@ homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/deconz.py @kane610 homeassistant/components/ecovacs.py @OverloadUT homeassistant/components/*/ecovacs.py @OverloadUT +homeassistant/components/edp_redy.py @abmantis +homeassistant/components/*/edp_redy.py @abmantis homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p +homeassistant/components/huawei_lte.py @scop +homeassistant/components/*/huawei_lte.py @scop homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 homeassistant/components/konnected.py @heythisisnate @@ -116,9 +122,13 @@ homeassistant/components/*/tesla.py @zabuldon homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tradfri.py @ggravlingen +homeassistant/components/upcloud.py @scop +homeassistant/components/*/upcloud.py @scop homeassistant/components/velux.py @Julius2342 homeassistant/components/*/velux.py @Julius2342 homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi +homeassistant/components/zoneminder.py @rohankapoorcom +homeassistant/components/*/zoneminder.py @rohankapoorcom homeassistant/scripts/check_config.py @kellerza diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86e212bb11d..fbe77c7756f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,5 +10,5 @@ The process is straight-forward. - Ensure tests work. - Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant. -Still interested? Then you should take a peek at the [developer documentation](https://home-assistant.io/developers/) to get more details. +Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details. diff --git a/LICENSE.md b/LICENSE.md index b62a9b5ff78..261eeb9e9f8 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,194 +1,201 @@ -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 -### Terms and Conditions for use, reproduction, and distribution + 1. Definitions. -#### 1. Definitions + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -“License” shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -“Licensor” shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -“Legal Entity” shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, “control” means **(i)** the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the -outstanding shares, or **(iii)** beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -“You” (or “Your”) shall mean an individual or Legal Entity exercising -permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -“Source” form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -“Object” form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -“Work” shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -“Derivative Works” shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -“Contribution” shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -“submitted” means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as “Not a Contribution.” + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -“Contributor” shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -#### 2. Grant of Copyright License + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: -#### 3. Grant of Patent License + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and -#### 4. Redistribution + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -* **(a)** You must give any other recipients of the Work or Derivative Works a copy of -this License; and -* **(b)** You must cause any modified files to carry prominent notices stating that You -changed the files; and -* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -#### 5. Submission of Contributions + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -#### 6. Trademarks + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -#### 7. Disclaimer of Warranty + END OF TERMS AND CONDITIONS -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. + APPENDIX: How to apply the Apache License to your work. -#### 8. Limitation of Liability + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. + Copyright [yyyy] [name of copyright owner] -#### 9. Accepting Warranty or Additional Liability + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. + http://www.apache.org/licenses/LICENSE-2.0 -_END OF TERMS AND CONDITIONS_ - -### APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets `[]` replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same “printed page” as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.rst b/README.rst index 6cf19d89c3c..4f459162a7e 100644 --- a/README.rst +++ b/README.rst @@ -21,8 +21,8 @@ Featured integrations |screenshot-components| -The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture `__ and the `section on creating your own -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 `__ and the `section on creating your own +components `__. If you run into issues while using Home Assistant or during development of a component, check the `Home Assistant help section `__ of our website for further help and information. diff --git a/docs/source/index.rst b/docs/source/index.rst index a6157dc7aac..c592f66c070 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,4 +19,4 @@ Indices and tables * :ref:`modindex` * :ref:`search` -.. _Home Assistant developers: https://home-assistant.io/developers/ +.. _Home Assistant developers: https://developers.home-assistant.io/ diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 65b1cd2ae1a..91b7a7f8466 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -7,7 +7,6 @@ import platform import subprocess import sys import threading - from typing import List, Dict, Any # noqa pylint: disable=unused-import @@ -20,15 +19,19 @@ from homeassistant.const import ( ) -def attempt_use_uvloop() -> None: +def set_loop() -> None: """Attempt to use uvloop.""" import asyncio - try: - import uvloop - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - except ImportError: - pass + if sys.platform == 'win32': + asyncio.set_event_loop(asyncio.ProactorEventLoop()) + else: + try: + import uvloop + except ImportError: + pass + else: + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) def validate_python() -> None: @@ -240,51 +243,39 @@ def cmdline() -> List[str]: return [arg for arg in sys.argv if arg != '--daemon'] -def setup_and_run_hass(config_dir: str, - args: argparse.Namespace) -> int: +async def setup_and_run_hass(config_dir: str, + args: argparse.Namespace) -> int: """Set up HASS and run.""" - from homeassistant import bootstrap + from homeassistant import bootstrap, core - # Run a simple daemon runner process on Windows to handle restarts - if os.name == 'nt' and '--runner' not in sys.argv: - nt_args = cmdline() + ['--runner'] - while True: - try: - subprocess.check_call(nt_args) - sys.exit(0) - except subprocess.CalledProcessError as exc: - if exc.returncode != RESTART_EXIT_CODE: - sys.exit(exc.returncode) + hass = core.HomeAssistant() if args.demo_mode: config = { 'frontend': {}, 'demo': {} } # type: Dict[str, Any] - hass = bootstrap.from_config_dict( - config, config_dir=config_dir, verbose=args.verbose, + bootstrap.async_from_config_dict( + config, hass, config_dir=config_dir, verbose=args.verbose, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, log_file=args.log_file, log_no_color=args.log_no_color) else: config_file = ensure_config_file(config_dir) print('Config directory:', config_dir) - hass = bootstrap.from_config_file( - config_file, verbose=args.verbose, skip_pip=args.skip_pip, + await bootstrap.async_from_config_file( + config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, log_file=args.log_file, log_no_color=args.log_no_color) - if hass is None: - return -1 - if args.open_ui: # Imported here to avoid importing asyncio before monkey patch from homeassistant.util.async_ import run_callback_threadsafe def open_browser(_: Any) -> None: """Open the web interface in a browser.""" - if hass.config.api is not None: # type: ignore + if hass.config.api is not None: import webbrowser - webbrowser.open(hass.config.api.base_url) # type: ignore + webbrowser.open(hass.config.api.base_url) run_callback_threadsafe( hass.loop, @@ -292,7 +283,7 @@ def setup_and_run_hass(config_dir: str, EVENT_HOMEASSISTANT_START, open_browser ) - return hass.start() + return await hass.async_run() def try_to_restart() -> None: @@ -347,7 +338,20 @@ def main() -> int: monkey_patch.disable_c_asyncio() monkey_patch.patch_weakref_tasks() - attempt_use_uvloop() + set_loop() + + # Run a simple daemon runner process on Windows to handle restarts + if os.name == 'nt' and '--runner' not in sys.argv: + nt_args = cmdline() + ['--runner'] + while True: + try: + subprocess.check_call(nt_args) + sys.exit(0) + except KeyboardInterrupt: + sys.exit(0) + except subprocess.CalledProcessError as exc: + if exc.returncode != RESTART_EXIT_CODE: + sys.exit(exc.returncode) args = get_arguments() @@ -366,11 +370,12 @@ def main() -> int: if args.pid_file: write_pid(args.pid_file) - exit_code = setup_and_run_hass(config_dir, args) + from homeassistant.util.async_ import asyncio_run + exit_code = asyncio_run(setup_and_run_hass(config_dir, args)) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() - return exit_code + return exit_code # type: ignore # mypy cannot yet infer it if __name__ == "__main__": diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 4ef8440de62..c6f978640f6 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -2,11 +2,13 @@ import asyncio import logging from collections import OrderedDict +from datetime import timedelta from typing import Any, Dict, List, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow +from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import callback, HomeAssistant from homeassistant.util import dt as dt_util @@ -242,8 +244,12 @@ class AuthManager: modules[module_id] = module.name return modules - async def async_create_refresh_token(self, user: models.User, - client_id: Optional[str] = None) \ + async def async_create_refresh_token( + self, user: models.User, client_id: Optional[str] = None, + client_name: Optional[str] = None, + client_icon: Optional[str] = None, + token_type: Optional[str] = None, + access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \ -> models.RefreshToken: """Create a new refresh token for a user.""" if not user.is_active: @@ -254,10 +260,36 @@ class AuthManager: 'System generated users cannot have refresh tokens connected ' 'to a client.') - if not user.system_generated and client_id is None: + if token_type is None: + if user.system_generated: + token_type = models.TOKEN_TYPE_SYSTEM + else: + token_type = models.TOKEN_TYPE_NORMAL + + if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM): + raise ValueError( + 'System generated users can only have system type ' + 'refresh tokens') + + if token_type == models.TOKEN_TYPE_NORMAL and client_id is None: raise ValueError('Client is required to generate a refresh token.') - return await self._store.async_create_refresh_token(user, client_id) + if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and + client_name is None): + raise ValueError('Client_name is required for long-lived access ' + 'token') + + if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN: + for token in user.refresh_tokens.values(): + if (token.client_name == client_name and token.token_type == + models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN): + # Each client_name can only have one + # long_lived_access_token type of refresh token + raise ValueError('{} already exists'.format(client_name)) + + return await self._store.async_create_refresh_token( + user, client_id, client_name, client_icon, + token_type, access_token_expiration) async def async_get_refresh_token( self, token_id: str) -> Optional[models.RefreshToken]: @@ -277,13 +309,17 @@ class AuthManager: @callback def async_create_access_token(self, - refresh_token: models.RefreshToken) -> str: + refresh_token: models.RefreshToken, + remote_ip: Optional[str] = None) -> str: """Create a new access token.""" + self._store.async_log_refresh_token_usage(refresh_token, remote_ip) + # pylint: disable=no-self-use + now = dt_util.utcnow() return jwt.encode({ 'iss': refresh_token.id, - 'iat': dt_util.utcnow(), - 'exp': dt_util.utcnow() + refresh_token.access_token_expiration, + 'iat': now, + 'exp': now + refresh_token.access_token_expiration, }, refresh_token.jwt_key, algorithm='HS256').decode() async def async_validate_access_token( diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 0f12d69211c..c170344746e 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -5,6 +5,7 @@ from logging import getLogger from typing import Any, Dict, List, Optional # noqa: F401 import hmac +from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util @@ -27,7 +28,8 @@ class AuthStore: """Initialize the auth store.""" self.hass = hass self._users = None # type: Optional[Dict[str, models.User]] - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) async def async_get_users(self) -> List[models.User]: """Retrieve all users.""" @@ -128,11 +130,27 @@ class AuthStore: self._async_schedule_save() async def async_create_refresh_token( - self, user: models.User, client_id: Optional[str] = None) \ + self, user: models.User, client_id: Optional[str] = None, + client_name: Optional[str] = None, + client_icon: Optional[str] = None, + token_type: str = models.TOKEN_TYPE_NORMAL, + access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \ -> models.RefreshToken: """Create a new token for a user.""" - refresh_token = models.RefreshToken(user=user, client_id=client_id) + kwargs = { + 'user': user, + 'client_id': client_id, + 'token_type': token_type, + 'access_token_expiration': access_token_expiration + } # type: Dict[str, Any] + if client_name: + kwargs['client_name'] = client_name + if client_icon: + kwargs['client_icon'] = client_icon + + refresh_token = models.RefreshToken(**kwargs) user.refresh_tokens[refresh_token.id] = refresh_token + self._async_schedule_save() return refresh_token @@ -178,6 +196,15 @@ class AuthStore: return found + @callback + def async_log_refresh_token_usage( + self, refresh_token: models.RefreshToken, + remote_ip: Optional[str] = None) -> None: + """Update refresh token last used information.""" + refresh_token.last_used_at = dt_util.utcnow() + refresh_token.last_used_ip = remote_ip + self._async_schedule_save() + async def _async_load(self) -> None: """Load the users.""" data = await self._store.async_load() @@ -216,15 +243,36 @@ class AuthStore: 'Ignoring refresh token %(id)s with invalid created_at ' '%(created_at)s for user_id %(user_id)s', rt_dict) continue + + token_type = rt_dict.get('token_type') + if token_type is None: + if rt_dict['client_id'] is None: + token_type = models.TOKEN_TYPE_SYSTEM + else: + token_type = models.TOKEN_TYPE_NORMAL + + # old refresh_token don't have last_used_at (pre-0.78) + last_used_at_str = rt_dict.get('last_used_at') + if last_used_at_str: + last_used_at = dt_util.parse_datetime(last_used_at_str) + else: + last_used_at = None + token = models.RefreshToken( id=rt_dict['id'], user=users[rt_dict['user_id']], client_id=rt_dict['client_id'], + # use dict.get to keep backward compatibility + client_name=rt_dict.get('client_name'), + client_icon=rt_dict.get('client_icon'), + token_type=token_type, created_at=created_at, access_token_expiration=timedelta( seconds=rt_dict['access_token_expiration']), token=rt_dict['token'], - jwt_key=rt_dict['jwt_key'] + jwt_key=rt_dict['jwt_key'], + last_used_at=last_used_at, + last_used_ip=rt_dict.get('last_used_ip'), ) users[rt_dict['user_id']].refresh_tokens[token.id] = token @@ -271,11 +319,18 @@ class AuthStore: 'id': refresh_token.id, 'user_id': user.id, 'client_id': refresh_token.client_id, + 'client_name': refresh_token.client_name, + 'client_icon': refresh_token.client_icon, + 'token_type': refresh_token.token_type, 'created_at': refresh_token.created_at.isoformat(), 'access_token_expiration': refresh_token.access_token_expiration.total_seconds(), 'token': refresh_token.token, 'jwt_key': refresh_token.jwt_key, + 'last_used_at': + refresh_token.last_used_at.isoformat() + if refresh_token.last_used_at else None, + 'last_used_ip': refresh_token.last_used_ip, } for user in self._users.values() for refresh_token in user.refresh_tokens.values() diff --git a/homeassistant/auth/const.py b/homeassistant/auth/const.py index 082d8966275..2e57986958c 100644 --- a/homeassistant/auth/const.py +++ b/homeassistant/auth/const.py @@ -2,3 +2,4 @@ from datetime import timedelta ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) +MFA_SESSION_EXPIRATION = timedelta(minutes=5) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 603ca6ff3b1..1746ef38f95 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -1,5 +1,4 @@ """Plugable auth modules for Home Assistant.""" -from datetime import timedelta import importlib import logging import types @@ -23,8 +22,6 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({ vol.Optional(CONF_ID): str, }, extra=vol.ALLOW_EXTRA) -SESSION_EXPIRATION = timedelta(minutes=5) - DATA_REQS = 'mfa_auth_module_reqs_processed' _LOGGER = logging.getLogger(__name__) @@ -34,6 +31,7 @@ class MultiFactorAuthModule: """Multi-factor Auth Module of validation function.""" DEFAULT_TITLE = 'Unnamed auth module' + MAX_RETRY_TIME = 3 def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize an auth module.""" @@ -84,7 +82,7 @@ class MultiFactorAuthModule: """Return whether user is setup.""" raise NotImplementedError - async def async_validation( + async def async_validate( self, user_id: str, user_input: Dict[str, Any]) -> bool: """Return True if validation passed.""" raise NotImplementedError diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index 9c72111ef96..9804cbcf635 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -77,7 +77,7 @@ class InsecureExampleModule(MultiFactorAuthModule): return True return False - async def async_validation( + async def async_validate( self, user_id: str, user_input: Dict[str, Any]) -> bool: """Return True if validation passed.""" for data in self._data: diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py new file mode 100644 index 00000000000..03be4c74d32 --- /dev/null +++ b/homeassistant/auth/mfa_modules/notify.py @@ -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, + ) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 0914658a655..9b5896ef666 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -60,13 +60,14 @@ class TotpAuthModule(MultiFactorAuthModule): """Auth module validate time-based one time password.""" DEFAULT_TITLE = 'Time-based One Time Password' + MAX_RETRY_TIME = 5 def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) self._users = None # type: Optional[Dict[str, str]] self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY) + STORAGE_VERSION, STORAGE_KEY, private=True) @property def input_schema(self) -> vol.Schema: @@ -130,7 +131,7 @@ class TotpAuthModule(MultiFactorAuthModule): return user_id in self._users # type: ignore - async def async_validation( + async def async_validate( self, user_id: str, user_input: Dict[str, Any]) -> bool: """Return True if validation passed.""" if self._users is None: @@ -149,10 +150,10 @@ class TotpAuthModule(MultiFactorAuthModule): if ota_secret is None: # even we cannot find user, we still do verify # to make timing the same as if user was found. - pyotp.TOTP(DUMMY_SECRET).verify(code) + pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1) return False - return bool(pyotp.TOTP(ota_secret).verify(code)) + return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1)) class TotpSetupFlow(SetupFlow): diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index a6500510e0d..b0f4024c3ab 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -7,9 +7,12 @@ import attr from homeassistant.util import dt as dt_util -from .const import ACCESS_TOKEN_EXPIRATION from .util import generate_secret +TOKEN_TYPE_NORMAL = 'normal' +TOKEN_TYPE_SYSTEM = 'system' +TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token' + @attr.s(slots=True) class User: @@ -37,23 +40,31 @@ class RefreshToken: """RefreshToken for a user to grant new access tokens.""" user = attr.ib(type=User) - client_id = attr.ib(type=str) # type: Optional[str] + client_id = attr.ib(type=Optional[str]) + access_token_expiration = attr.ib(type=timedelta) + client_name = attr.ib(type=Optional[str], default=None) + client_icon = attr.ib(type=Optional[str], default=None) + token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL, + validator=attr.validators.in_(( + TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, + TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN))) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) - access_token_expiration = attr.ib(type=timedelta, - default=ACCESS_TOKEN_EXPIRATION) token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64))) jwt_key = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64))) + last_used_at = attr.ib(type=Optional[datetime], default=None) + last_used_ip = attr.ib(type=Optional[str], default=None) + @attr.s(slots=True) class Credentials: """Credentials for a user on an auth provider.""" auth_provider_type = attr.ib(type=str) - auth_provider_id = attr.ib(type=str) # type: Optional[str] + auth_provider_id = attr.ib(type=Optional[str]) # Allow the auth provider to store data to represent their auth. data = attr.ib(type=dict) diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 3cb1c6b121e..e96f6d7ebba 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -15,8 +15,8 @@ from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry from ..auth_store import AuthStore +from ..const import MFA_SESSION_EXPIRATION from ..models import Credentials, User, UserMeta # noqa: F401 -from ..mfa_modules import SESSION_EXPIRATION _LOGGER = logging.getLogger(__name__) DATA_REQS = 'auth_prov_reqs_processed' @@ -171,6 +171,7 @@ class LoginFlow(data_entry_flow.FlowHandler): self._auth_manager = auth_provider.hass.auth # type: ignore self.available_mfa_modules = {} # type: Dict[str, str] self.created_at = dt_util.utcnow() + self.invalid_mfa_times = 0 self.user = None # type: Optional[User] async def async_step_init( @@ -212,6 +213,8 @@ class LoginFlow(data_entry_flow.FlowHandler): self, user_input: Optional[Dict[str, str]] = None) \ -> Dict[str, Any]: """Handle the step of mfa validation.""" + assert self.user + errors = {} auth_module = self._auth_manager.get_auth_mfa_module( @@ -221,25 +224,34 @@ class LoginFlow(data_entry_flow.FlowHandler): # will show invalid_auth_module error return await self.async_step_select_mfa_module(user_input={}) + if user_input is None and hasattr(auth_module, + 'async_initialize_login_mfa_step'): + await auth_module.async_initialize_login_mfa_step(self.user.id) + if user_input is not None: - expires = self.created_at + SESSION_EXPIRATION + expires = self.created_at + MFA_SESSION_EXPIRATION if dt_util.utcnow() > expires: return self.async_abort( reason='login_expired' ) - result = await auth_module.async_validation( - self.user.id, user_input) # type: ignore + result = await auth_module.async_validate( + self.user.id, user_input) if not result: errors['base'] = 'invalid_code' + self.invalid_mfa_times += 1 + if self.invalid_mfa_times >= auth_module.MAX_RETRY_TIME > 0: + return self.async_abort( + reason='too_many_retry' + ) if not errors: return await self.async_finish(self.user) description_placeholders = { 'mfa_module_name': auth_module.name, - 'mfa_module_id': auth_module.id - } # type: Dict[str, str] + 'mfa_module_id': auth_module.id, + } # type: Dict[str, Optional[str]] return self.async_show_form( step_id='mfa', diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index c743a5b7f65..8710e7c60bc 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -52,7 +52,8 @@ class Data: def __init__(self, hass: HomeAssistant) -> None: """Initialize the user data store.""" self.hass = hass - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) self._data = None # type: Optional[Dict[str, Any]] async def async_load(self) -> None: diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index f631f8e73cf..111b9e7d39f 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -24,7 +24,7 @@ USER_SCHEMA = vol.Schema({ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA) -LEGACY_USER = 'homeassistant' +LEGACY_USER_NAME = 'Legacy API password user' class InvalidAuthError(HomeAssistantError): @@ -52,23 +52,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider): async def async_get_or_create_credentials( self, flow_result: Dict[str, str]) -> Credentials: - """Return LEGACY_USER always.""" - for credential in await self.async_credentials(): - if credential.data['username'] == LEGACY_USER: - return credential + """Return credentials for this login.""" + credentials = await self.async_credentials() + if credentials: + return credentials[0] - return self.async_create_credentials({ - 'username': LEGACY_USER - }) + return self.async_create_credentials({}) async def async_user_meta_for_credentials( self, credentials: Credentials) -> UserMeta: """ - Set name as LEGACY_USER always. + Return info for the user. Will be used to populate info when creating a new user. """ - return UserMeta(name=LEGACY_USER, is_active=True) + return UserMeta(name=LEGACY_USER_NAME, is_active=True) class LegacyLoginFlow(LoginFlow): diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 2051359c0ba..0676cec7fad 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -5,7 +5,6 @@ import os import sys from time import time from collections import OrderedDict - from typing import Any, Optional, Dict import voluptuous as vol @@ -19,7 +18,6 @@ from homeassistant.util.logging import AsyncHandler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.signal import async_register_signal_handling _LOGGER = logging.getLogger(__name__) @@ -160,7 +158,6 @@ async def async_from_config_dict(config: Dict[str, Any], stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop-start) - async_register_signal_handling(hass) return hass diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 63977ed88c7..c5110a2ad5a 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -14,7 +14,6 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) -from homeassistant.loader import bind_hass from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -32,78 +31,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def alarm_disarm(hass, code=None, entity_id=None): - """Send the alarm the command for disarm.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) - - -@bind_hass -def alarm_arm_home(hass, code=None, entity_id=None): - """Send the alarm the command for arm home.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) - - -@bind_hass -def alarm_arm_away(hass, code=None, entity_id=None): - """Send the alarm the command for arm away.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) - - -@bind_hass -def alarm_arm_night(hass, code=None, entity_id=None): - """Send the alarm the command for arm night.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) - - -@bind_hass -def alarm_trigger(hass, code=None, entity_id=None): - """Send the alarm the command for trigger.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) - - -@bind_hass -def alarm_arm_custom_bypass(hass, code=None, entity_id=None): - """Send the alarm the command for arm custom bypass.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data) - - @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for sensors.""" diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index e5ad54c4147..d8193f958da 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -18,10 +18,13 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, CONF_NAME, CONF_CODE) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - CONF_RETAIN, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -46,13 +49,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT Alarm Control Panel platform.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT alarm control panel through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT alarm control panel dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add an MQTT alarm control panel.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(alarm.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -65,18 +83,22 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_CODE), config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE))]) + config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash,)]) -class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): +class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, + alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" def __init__(self, name, state_topic, command_topic, qos, retain, payload_disarm, payload_arm_home, payload_arm_away, code, - availability_topic, payload_available, payload_not_available): + availability_topic, payload_available, payload_not_available, + discovery_hash): """Init the MQTT Alarm Control Panel.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -87,11 +109,13 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away self._code = code + self._discovery_hash = discovery_hash @asyncio.coroutine def async_added_to_hass(self): """Subscribe mqtt events.""" - yield from super().async_added_to_hass() + yield from MqttAvailability.async_added_to_hass(self) + yield from MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index 2aa157a5cad..9150518022f 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -4,71 +4,65 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.spc/ """ -import asyncio import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback from homeassistant.components.spc import ( - ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway) + ATTR_DISCOVER_AREAS, DATA_API, SIGNAL_UPDATE_ALARM) from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_UNKNOWN) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) _LOGGER = logging.getLogger(__name__) -SPC_AREA_MODE_TO_STATE = { - '0': STATE_ALARM_DISARMED, - '1': STATE_ALARM_ARMED_HOME, - '3': STATE_ALARM_ARMED_AWAY, -} - -def _get_alarm_state(spc_mode): +def _get_alarm_state(area): """Get the alarm state.""" - return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN) + from pyspcwebgw.const import AreaMode + + if area.verified_alarm: + return STATE_ALARM_TRIGGERED + + mode_to_state = { + AreaMode.UNSET: STATE_ALARM_DISARMED, + AreaMode.PART_SET_A: STATE_ALARM_ARMED_HOME, + AreaMode.PART_SET_B: STATE_ALARM_ARMED_NIGHT, + AreaMode.FULL_SET: STATE_ALARM_ARMED_AWAY, + } + return mode_to_state.get(area.mode) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the SPC alarm control panel platform.""" if (discovery_info is None or discovery_info[ATTR_DISCOVER_AREAS] is None): return - api = hass.data[DATA_API] - devices = [SpcAlarm(api, area) - for area in discovery_info[ATTR_DISCOVER_AREAS]] - - async_add_entities(devices) + async_add_entities([SpcAlarm(area=area, api=hass.data[DATA_API]) + for area in discovery_info[ATTR_DISCOVER_AREAS]]) class SpcAlarm(alarm.AlarmControlPanel): """Representation of the SPC alarm panel.""" - def __init__(self, api, area): + def __init__(self, area, api): """Initialize the SPC alarm panel.""" - self._area_id = area['id'] - self._name = area['name'] - self._state = _get_alarm_state(area['mode']) - if self._state == STATE_ALARM_DISARMED: - self._changed_by = area.get('last_unset_user_name', 'unknown') - else: - self._changed_by = area.get('last_set_user_name', 'unknown') + self._area = area self._api = api - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call for adding new entities.""" - self.hass.data[DATA_REGISTRY].register_alarm_device( - self._area_id, self) + async_dispatcher_connect(self.hass, + SIGNAL_UPDATE_ALARM.format(self._area.id), + self._update_callback) - @asyncio.coroutine - def async_update_from_spc(self, state, extra): - """Update the alarm panel with a new state.""" - self._state = state - self._changed_by = extra.get('changed_by', 'unknown') - self.async_schedule_update_ha_state() + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) @property def should_poll(self): @@ -78,32 +72,34 @@ class SpcAlarm(alarm.AlarmControlPanel): @property def name(self): """Return the name of the device.""" - return self._name + return self._area.name @property def changed_by(self): """Return the user the last change was triggered by.""" - return self._changed_by + return self._area.last_changed_by @property def state(self): """Return the state of the device.""" - return self._state + return _get_alarm_state(self._area) - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command.""" - yield from self._api.send_area_command( - self._area_id, SpcWebGateway.AREA_COMMAND_UNSET) + from pyspcwebgw.const import AreaMode + self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET) - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - yield from self._api.send_area_command( - self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET) + from pyspcwebgw.const import AreaMode + self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A) - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_night(self, code=None): + """Send arm home command.""" + from pyspcwebgw.const import AreaMode + self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B) + + async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - yield from self._api.send_area_command( - self._area_id, SpcWebGateway.AREA_COMMAND_SET) + from pyspcwebgw.const import AreaMode + self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET) diff --git a/homeassistant/components/alarm_control_panel/yale_smart_alarm.py b/homeassistant/components/alarm_control_panel/yale_smart_alarm.py new file mode 100755 index 00000000000..e512d15fcdd --- /dev/null +++ b/homeassistant/components/alarm_control_panel/yale_smart_alarm.py @@ -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() diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index eab725c4653..176c286ebc3 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1529,3 +1529,8 @@ async def async_api_reportstate(hass, config, request, context, entity): name='StateReport', context={'properties': properties} ) + + +def turned_off_response(message): + """Return a device turned off response.""" + return api_error(message[API_DIRECTIVE], error_type='BRIDGE_UNREACHABLE') diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index 97fb2363024..21ff0e3286d 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/apple_tv/ """ import asyncio import logging - from typing import Sequence, TypeVar, Union import voluptuous as vol diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index c6a414b9d91..015e1e0d1fc 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -61,10 +61,12 @@ def setup(hass, config): arlo_base_station = next(( station for station in arlo.base_stations), None) - if arlo_base_station is None: + if arlo_base_station is not None: + arlo_base_station.refresh_rate = scan_interval.total_seconds() + elif not arlo.cameras: + _LOGGER.error("No Arlo camera or base station available.") return False - arlo_base_station.refresh_rate = scan_interval.total_seconds() hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: diff --git a/homeassistant/components/asterisk_mbox.py b/homeassistant/components/asterisk_mbox.py index 0d6d811db70..0907e48b256 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox.py @@ -13,7 +13,7 @@ from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) + async_dispatcher_send, dispatcher_connect) REQUIREMENTS = ['asterisk_mbox==0.5.0'] @@ -21,8 +21,11 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'asterisk_mbox' +SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform" SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' +SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated' +SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -41,9 +44,7 @@ def setup(hass, config): port = conf.get(CONF_PORT) password = conf.get(CONF_PASSWORD) - hass.data[DOMAIN] = AsteriskData(hass, host, port, password) - - discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config) + hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config) return True @@ -51,31 +52,71 @@ def setup(hass, config): class AsteriskData: """Store Asterisk mailbox data.""" - def __init__(self, hass, host, port, password): + def __init__(self, hass, host, port, password, config): """Init the Asterisk data object.""" from asterisk_mbox import Client as asteriskClient - self.hass = hass - self.client = asteriskClient(host, port, password, self.handle_data) - self.messages = [] + self.config = config + self.messages = None + self.cdr = None - async_dispatcher_connect( + dispatcher_connect( self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages) + dispatcher_connect( + self.hass, SIGNAL_CDR_REQUEST, self._request_cdr) + dispatcher_connect( + self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform) + # Only connect after signal connection to ensure we don't miss any + self.client = asteriskClient(host, port, password, self.handle_data) + + @callback + def _discover_platform(self, component): + _LOGGER.debug("Adding mailbox %s", component) + self.hass.async_create_task(discovery.async_load_platform( + self.hass, "mailbox", component, {}, self.config)) @callback def handle_data(self, command, msg): """Handle changes to the mailbox.""" - from asterisk_mbox.commands import CMD_MESSAGE_LIST + from asterisk_mbox.commands import (CMD_MESSAGE_LIST, + CMD_MESSAGE_CDR_AVAILABLE, + CMD_MESSAGE_CDR) if command == CMD_MESSAGE_LIST: - _LOGGER.debug("AsteriskVM sent updated message list") + _LOGGER.debug("AsteriskVM sent updated message list: Len %d", + len(msg)) + old_messages = self.messages self.messages = sorted( msg, key=lambda item: item['info']['origtime'], reverse=True) - async_dispatcher_send( - self.hass, SIGNAL_MESSAGE_UPDATE, self.messages) + if not isinstance(old_messages, list): + async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, + DOMAIN) + async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, + self.messages) + elif command == CMD_MESSAGE_CDR: + _LOGGER.debug("AsteriskVM sent updated CDR list: Len %d", + len(msg.get('entries', []))) + self.cdr = msg['entries'] + async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr) + elif command == CMD_MESSAGE_CDR_AVAILABLE: + if not isinstance(self.cdr, list): + _LOGGER.debug("AsteriskVM adding CDR platform") + self.cdr = [] + async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, + "asterisk_cdr") + async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST) + else: + _LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d", + command, len(msg)) @callback def _request_messages(self): """Handle changes to the mailbox.""" _LOGGER.debug("Requesting message list") self.client.messages() + + @callback + def _request_cdr(self): + """Handle changes to the CDR.""" + _LOGGER.debug("Requesting CDR list") + self.client.get_cdr() diff --git a/homeassistant/components/auth/.translations/ca.json b/homeassistant/components/auth/.translations/ca.json index 1b3b25dbcff..f4318a0eb21 100644 --- a/homeassistant/components/auth/.translations/ca.json +++ b/homeassistant/components/auth/.translations/ca.json @@ -1,8 +1,27 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles." + }, + "error": { + "invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho." + }, + "step": { + "init": { + "description": "Seleccioneu un dels serveis de notificaci\u00f3:", + "title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions" + }, + "setup": { + "description": "**notify.{notify_service}** ha enviat una contrasenya d'un sol \u00fas. Introdu\u00efu-la a continuaci\u00f3:", + "title": "Verifiqueu la configuraci\u00f3" + } + }, + "title": "Contrasenya d'un sol \u00fas del servei de notificacions" + }, "totp": { "error": { - "invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa." + "invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa." }, "step": { "init": { diff --git a/homeassistant/components/auth/.translations/de.json b/homeassistant/components/auth/.translations/de.json index 67f948e8340..21c83290629 100644 --- a/homeassistant/components/auth/.translations/de.json +++ b/homeassistant/components/auth/.translations/de.json @@ -1,8 +1,26 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Keine Benachrichtigungsdienste verf\u00fcgbar." + }, + "error": { + "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut." + }, + "step": { + "init": { + "description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:", + "title": "Einmal Passwort f\u00fcr Notify einrichten" + }, + "setup": { + "description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:", + "title": "\u00dcberpr\u00fcfe das Setup" + } + } + }, "totp": { "error": { - "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn Sie diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist." + "invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn du diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist." }, "step": { "init": { diff --git a/homeassistant/components/auth/.translations/en.json b/homeassistant/components/auth/.translations/en.json index a0fd20e9d08..66c0e92d9b5 100644 --- a/homeassistant/components/auth/.translations/en.json +++ b/homeassistant/components/auth/.translations/en.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No notification services available." + }, + "error": { + "invalid_code": "Invalid code, please try again." + }, + "step": { + "init": { + "description": "Please select one of the notification services:", + "title": "Set up one-time password delivered by notify component" + }, + "setup": { + "description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:", + "title": "Verify setup" + } + }, + "title": "Notify One-Time Password" + }, "totp": { "error": { "invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate." diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json index e8a8037c39a..85540314af0 100644 --- a/homeassistant/components/auth/.translations/fr.json +++ b/homeassistant/components/auth/.translations/fr.json @@ -1,8 +1,15 @@ { "mfa_setup": { + "notify": { + "step": { + "setup": { + "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :" + } + } + }, "totp": { "error": { - "invalid_code": "Code invalide. S'il vous pla\u00eet essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte." + "invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte." }, "step": { "init": { diff --git a/homeassistant/components/auth/.translations/he.json b/homeassistant/components/auth/.translations/he.json new file mode 100644 index 00000000000..bc1826d4d79 --- /dev/null +++ b/homeassistant/components/auth/.translations/he.json @@ -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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/hu.json b/homeassistant/components/auth/.translations/hu.json new file mode 100644 index 00000000000..4500098553e --- /dev/null +++ b/homeassistant/components/auth/.translations/hu.json @@ -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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/id.json b/homeassistant/components/auth/.translations/id.json new file mode 100644 index 00000000000..f6a22386f99 --- /dev/null +++ b/homeassistant/components/auth/.translations/id.json @@ -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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 4eb4783edd9..e1f26e88bc7 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -1,12 +1,31 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "init": { + "description": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", + "title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815" + }, + "setup": { + "description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574 \uc8fc\uc138\uc694:", + "title": "\uc124\uc815 \ud655\uc778" + } + }, + "title": "\uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc54c\ub9bc" + }, "totp": { "error": { "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694." }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/auth/.translations/lb.json b/homeassistant/components/auth/.translations/lb.json index f55ae4b97ba..12ced930446 100644 --- a/homeassistant/components/auth/.translations/lb.json +++ b/homeassistant/components/auth/.translations/lb.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Keen Notifikatioun's D\u00e9ngscht disponibel." + }, + "error": { + "invalid_code": "Ong\u00ebltege Code, prob\u00e9iert w.e.g. nach emol." + }, + "step": { + "init": { + "description": "Wielt w.e.g. een Notifikatioun's D\u00e9ngscht aus:", + "title": "Eemolegt Passwuert ariichte wat vun engem Notifikatioun's Komponente versch\u00e9ckt g\u00ebtt" + }, + "setup": { + "description": "Een eemolegt Passwuert ass vun **notify.{notify_service}** gesch\u00e9ckt ginn. Gitt et w.e.g hei \u00ebnnen dr\u00ebnner an:", + "title": "Astellungen iwwerpr\u00e9iwen" + } + }, + "title": "Eemolegt Passwuert Notifikatioun" + }, "totp": { "error": { "invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass." diff --git a/homeassistant/components/auth/.translations/nn.json b/homeassistant/components/auth/.translations/nn.json new file mode 100644 index 00000000000..24d756f938b --- /dev/null +++ b/homeassistant/components/auth/.translations/nn.json @@ -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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/no.json b/homeassistant/components/auth/.translations/no.json index 43ec497cfb1..48b5db8a3b6 100644 --- a/homeassistant/components/auth/.translations/no.json +++ b/homeassistant/components/auth/.translations/no.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Ingen varslingstjenester er tilgjengelig." + }, + "error": { + "invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen." + }, + "step": { + "init": { + "description": "Vennligst velg en av varslingstjenestene:", + "title": "Sett opp engangspassord levert av varsel komponent" + }, + "setup": { + "description": "Et engangspassord har blitt sendt via **notify.{notify_service}**. Vennligst skriv det inn nedenfor:", + "title": "Bekreft oppsettet" + } + }, + "title": "Varsle engangspassord" + }, "totp": { "error": { "invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig." diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json index 78999c34c22..3e320ba8d62 100644 --- a/homeassistant/components/auth/.translations/pl.json +++ b/homeassistant/components/auth/.translations/pl.json @@ -1,5 +1,23 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Brak dost\u0119pnych us\u0142ug powiadamiania." + }, + "error": { + "invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie." + }, + "step": { + "init": { + "description": "Prosz\u0119 wybra\u0107 jedn\u0105 z us\u0142ugi powiadamiania:", + "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144" + }, + "setup": { + "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez ** powiadom. {notify_service} **. Wpisz je poni\u017cej:", + "title": "Sprawd\u017a konfiguracj\u0119" + } + } + }, "totp": { "error": { "invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy." diff --git a/homeassistant/components/auth/.translations/pt-BR.json b/homeassistant/components/auth/.translations/pt-BR.json new file mode 100644 index 00000000000..58c785a5b95 --- /dev/null +++ b/homeassistant/components/auth/.translations/pt-BR.json @@ -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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ru.json b/homeassistant/components/auth/.translations/ru.json index a716425f345..edf136bd7f3 100644 --- a/homeassistant/components/auth/.translations/ru.json +++ b/homeassistant/components/auth/.translations/ru.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439." + }, + "error": { + "invalid_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." + }, + "step": { + "init": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u0443 \u0438\u0437 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439:", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439" + }, + "setup": { + "description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u0447\u0435\u0440\u0435\u0437 **notify.{notify_service}**. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u043d\u0438\u0436\u0435:", + "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443" + } + }, + "title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439" + }, "totp": { "error": { "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f." diff --git a/homeassistant/components/auth/.translations/sl.json b/homeassistant/components/auth/.translations/sl.json index 45b57a772f9..2efc23f78f6 100644 --- a/homeassistant/components/auth/.translations/sl.json +++ b/homeassistant/components/auth/.translations/sl.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Ni na voljo storitev obve\u0161\u010danja." + }, + "error": { + "invalid_code": "Neveljavna koda, poskusite znova." + }, + "step": { + "init": { + "description": "Prosimo, izberite eno od storitev obve\u0161\u010danja:", + "title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento" + }, + "setup": { + "description": "Enkratno geslo je poslal **notify.{notify_service} **. Vnesite ga spodaj:", + "title": "Preverite nastavitev" + } + }, + "title": "Obvesti Enkratno Geslo" + }, "totp": { "error": { "invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna." diff --git a/homeassistant/components/auth/.translations/sv.json b/homeassistant/components/auth/.translations/sv.json new file mode 100644 index 00000000000..604ae3c4fe5 --- /dev/null +++ b/homeassistant/components/auth/.translations/sv.json @@ -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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/zh-Hans.json b/homeassistant/components/auth/.translations/zh-Hans.json index c5b397a8e12..1cb311f016f 100644 --- a/homeassistant/components/auth/.translations/zh-Hans.json +++ b/homeassistant/components/auth/.translations/zh-Hans.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u6ca1\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52a1\u3002" + }, + "error": { + "invalid_code": "\u4ee3\u7801\u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "description": "\u8bf7\u4ece\u4e0b\u9762\u9009\u62e9\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a", + "title": "\u8bbe\u7f6e\u7531\u901a\u77e5\u7ec4\u4ef6\u4f20\u9012\u7684\u4e00\u6b21\u6027\u5bc6\u7801" + }, + "setup": { + "description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}** \u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165\uff1a", + "title": "\u9a8c\u8bc1\u8bbe\u7f6e" + } + }, + "title": "\u4e00\u6b21\u6027\u5bc6\u7801\u901a\u77e5" + }, "totp": { "error": { "invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002" diff --git a/homeassistant/components/auth/.translations/zh-Hant.json b/homeassistant/components/auth/.translations/zh-Hant.json index ef41ea87248..e791f20a738 100644 --- a/homeassistant/components/auth/.translations/zh-Hant.json +++ b/homeassistant/components/auth/.translations/zh-Hant.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u6c92\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52d9\u3002" + }, + "error": { + "invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "init": { + "description": "\u8acb\u9078\u64c7\u4e00\u9805\u901a\u77e5\u670d\u52d9\uff1a", + "title": "\u8a2d\u5b9a\u4e00\u6b21\u6027\u5bc6\u78bc\u50b3\u9001\u7d44\u4ef6" + }, + "setup": { + "description": "\u4e00\u6b21\u6027\u5bc6\u78bc\u5df2\u900f\u904e **notify.{notify_service}** \u50b3\u9001\u3002\u8acb\u65bc\u4e0b\u65b9\u8f38\u5165\uff1a", + "title": "\u9a57\u8b49\u8a2d\u5b9a" + } + }, + "title": "\u901a\u77e5\u4e00\u6b21\u6027\u5bc6\u78bc" + }, "totp": { "error": { "invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002" diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index a87e646761c..c0027fac820 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -12,6 +12,7 @@ be in JSON as it's more readable. Exchange the authorization code retrieved from the login flow for tokens. { + "client_id": "https://hassbian.local:8123/", "grant_type": "authorization_code", "code": "411ee2f916e648d691e937ae9344681e" } @@ -32,6 +33,7 @@ token. Request a new access token using a refresh token. { + "client_id": "https://hassbian.local:8123/", "grant_type": "refresh_token", "refresh_token": "IJKLMNOPQRST" } @@ -55,6 +57,66 @@ ever been granted by that refresh token. Response code will ALWAYS be 200. "action": "revoke" } +# Websocket API + +## Get current user + +Send websocket command `auth/current_user` will return current user of the +active websocket connection. + +{ + "id": 10, + "type": "auth/current_user", +} + +The result payload likes + +{ + "id": 10, + "type": "result", + "success": true, + "result": { + "id": "USER_ID", + "name": "John Doe", + "is_owner': true, + "credentials": [ + { + "auth_provider_type": "homeassistant", + "auth_provider_id": null + } + ], + "mfa_modules": [ + { + "id": "totp", + "name": "TOTP", + "enabled": true, + } + ] + } +} + +## Create a long-lived access token + +Send websocket command `auth/long_lived_access_token` will create +a long-lived access token for current user. Access token will not be saved in +Home Assistant. User need to record the token in secure place. + +{ + "id": 11, + "type": "auth/long_lived_access_token", + "client_name": "GPS Logger", + "lifespan": 365 +} + +Result will be a long-lived access token: + +{ + "id": 11, + "type": "result", + "success": true, + "result": "ABCDEFGH" +} + """ import logging import uuid @@ -63,8 +125,10 @@ from datetime import timedelta from aiohttp import web import voluptuous as vol -from homeassistant.auth.models import User, Credentials +from homeassistant.auth.models import User, Credentials, \ + TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN from homeassistant.components import websocket_api +from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView @@ -83,6 +147,28 @@ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_CURRENT_USER, }) +WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token' +SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + vol.Required('lifespan'): int, # days + vol.Required('client_name'): str, + vol.Optional('client_icon'): str, + }) + +WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens' +SCHEMA_WS_REFRESH_TOKENS = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_REFRESH_TOKENS, + }) + +WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token' +SCHEMA_WS_DELETE_REFRESH_TOKEN = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN, + vol.Required('refresh_token_id'): str, + }) + RESULT_TYPE_CREDENTIALS = 'credentials' RESULT_TYPE_USER = 'user' @@ -100,6 +186,21 @@ async def async_setup(hass, config): WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER ) + hass.components.websocket_api.async_register_command( + WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + websocket_create_long_lived_access_token, + SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_REFRESH_TOKENS, + websocket_refresh_tokens, + SCHEMA_WS_REFRESH_TOKENS + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_DELETE_REFRESH_TOKEN, + websocket_delete_refresh_token, + SCHEMA_WS_DELETE_REFRESH_TOKEN + ) await login_flow.async_setup(hass, store_result) await mfa_setup_flow.async_setup(hass) @@ -135,10 +236,12 @@ class TokenView(HomeAssistantView): return await self._async_handle_revoke_token(hass, data) if grant_type == 'authorization_code': - return await self._async_handle_auth_code(hass, data) + return await self._async_handle_auth_code( + hass, data, str(request[KEY_REAL_IP])) if grant_type == 'refresh_token': - return await self._async_handle_refresh_token(hass, data) + return await self._async_handle_refresh_token( + hass, data, str(request[KEY_REAL_IP])) return self.json({ 'error': 'unsupported_grant_type', @@ -163,7 +266,7 @@ class TokenView(HomeAssistantView): await hass.auth.async_remove_refresh_token(refresh_token) return web.Response(status=200) - async def _async_handle_auth_code(self, hass, data): + async def _async_handle_auth_code(self, hass, data, remote_addr): """Handle authorization code request.""" client_id = data.get('client_id') if client_id is None or not indieauth.verify_client_id(client_id): @@ -199,7 +302,8 @@ class TokenView(HomeAssistantView): refresh_token = await hass.auth.async_create_refresh_token(user, client_id) - access_token = hass.auth.async_create_access_token(refresh_token) + access_token = hass.auth.async_create_access_token( + refresh_token, remote_addr) return self.json({ 'access_token': access_token, @@ -209,7 +313,7 @@ class TokenView(HomeAssistantView): int(refresh_token.access_token_expiration.total_seconds()), }) - async def _async_handle_refresh_token(self, hass, data): + async def _async_handle_refresh_token(self, hass, data, remote_addr): """Handle authorization code request.""" client_id = data.get('client_id') if client_id is not None and not indieauth.verify_client_id(client_id): @@ -237,7 +341,8 @@ class TokenView(HomeAssistantView): 'error': 'invalid_request', }, status_code=400) - access_token = hass.auth.async_create_access_token(refresh_token) + access_token = hass.auth.async_create_access_token( + refresh_token, remote_addr) return self.json({ 'access_token': access_token, @@ -343,3 +448,68 @@ def websocket_current_user( })) hass.async_create_task(async_get_current_user(connection.user)) + + +@websocket_api.ws_require_user() +@callback +def websocket_create_long_lived_access_token( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Create or a long-lived access token.""" + async def async_create_long_lived_access_token(user): + """Create or a long-lived access token.""" + refresh_token = await hass.auth.async_create_refresh_token( + user, + client_name=msg['client_name'], + client_icon=msg.get('client_icon'), + token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + access_token_expiration=timedelta(days=msg['lifespan'])) + + access_token = hass.auth.async_create_access_token( + refresh_token) + + connection.send_message_outside( + websocket_api.result_message(msg['id'], access_token)) + + hass.async_create_task( + async_create_long_lived_access_token(connection.user)) + + +@websocket_api.ws_require_user() +@callback +def websocket_refresh_tokens( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Return metadata of users refresh tokens.""" + current_id = connection.request.get('refresh_token_id') + connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{ + 'id': refresh.id, + 'client_id': refresh.client_id, + 'client_name': refresh.client_name, + 'client_icon': refresh.client_icon, + 'type': refresh.token_type, + 'created_at': refresh.created_at, + 'is_current': refresh.id == current_id, + 'last_used_at': refresh.last_used_at, + 'last_used_ip': refresh.last_used_ip, + } for refresh in connection.user.refresh_tokens.values()])) + + +@websocket_api.ws_require_user() +@callback +def websocket_delete_refresh_token( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Handle a delete refresh token request.""" + async def async_delete_refresh_token(user, refresh_token_id): + """Delete a refresh token.""" + refresh_token = connection.user.refresh_tokens.get(refresh_token_id) + + if refresh_token is None: + return websocket_api.error_message( + msg['id'], 'invalid_token_id', 'Received invalid token') + + await hass.auth.async_remove_refresh_token(refresh_token) + + connection.send_message_outside( + websocket_api.result_message(msg['id'], {})) + + hass.async_create_task( + async_delete_refresh_token(connection.user, msg['refresh_token_id'])) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index bcf73258ffa..30432a612a4 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,24 +1,13 @@ """Helpers to resolve client ID/secret.""" import asyncio +from ipaddress import ip_address from html.parser import HTMLParser -from ipaddress import ip_address, ip_network from urllib.parse import urlparse, urljoin import aiohttp from aiohttp.client_exceptions import ClientError -# IP addresses of loopback interfaces -ALLOWED_IPS = ( - ip_address('127.0.0.1'), - ip_address('::1'), -) - -# RFC1918 - Address allocation for Private Internets -ALLOWED_NETWORKS = ( - ip_network('10.0.0.0/8'), - ip_network('172.16.0.0/12'), - ip_network('192.168.0.0/16'), -) +from homeassistant.util.network import is_local async def verify_redirect_uri(hass, client_id, redirect_uri): @@ -185,9 +174,7 @@ def _parse_client_id(client_id): # Not an ip address pass - if (address is None or - address in ALLOWED_IPS or - any(address in network for network in ALLOWED_NETWORKS)): + if address is None or is_local(address): return parts raise ValueError('Hostname should be a domain name or local IP address') diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index a518bdde415..3a51cf8066f 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -66,7 +66,7 @@ associate with an credential if "type" set to "link_user" in "version": 1 } """ -import aiohttp.web +from aiohttp import web import voluptuous as vol from homeassistant import data_entry_flow @@ -95,11 +95,20 @@ class AuthProvidersView(HomeAssistantView): async def get(self, request): """Get available auth providers.""" + hass = request.app['hass'] + + if not hass.components.onboarding.async_is_onboarded(): + return self.json_message( + message='Onboarding not finished', + status_code=400, + message_code='onboarding_required' + ) + return self.json([{ 'name': provider.name, 'id': provider.id, 'type': provider.type, - } for provider in request.app['hass'].auth.auth_providers]) + } for provider in hass.auth.auth_providers]) def _prepare_result_json(result): @@ -139,7 +148,7 @@ class LoginFlowIndexView(HomeAssistantView): async def get(self, request): """Do not allow index of flows in progress.""" - return aiohttp.web.Response(status=405) + return web.Response(status=405) @RequestDataValidator(vol.Schema({ vol.Required('client_id'): str, @@ -217,8 +226,9 @@ class LoginFlowResourceView(HomeAssistantView): if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: # @log_invalid_auth does not work here since it returns HTTP 200 # need manually log failed login attempts - if result['errors'] is not None and \ - result['errors'].get('base') == 'invalid_auth': + if (result.get('errors') is not None and + result['errors'].get('base') in ['invalid_auth', + 'invalid_code']): await process_wrong_login(request) return self.json(_prepare_result_json(result)) diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index b0083ab577b..57f5ed659b0 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -11,6 +11,25 @@ "error": { "invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate." } + }, + "notify": { + "title": "Notify One-Time Password", + "step": { + "init": { + "title": "Set up one-time password delivered by notify component", + "description": "Please select one of the notification services:" + }, + "setup": { + "title": "Verify setup", + "description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:" + } + }, + "abort": { + "no_available_service": "No notification services available." + }, + "error": { + "invalid_code": "Invalid code, please try again." + } } } } diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c6c0af90d15..b34a6b7cf40 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -115,70 +115,26 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, entity_id=None): - """Toggle specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def trigger(hass, entity_id=None): - """Trigger specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TRIGGER, data) - - -@bind_hass -def reload(hass): - """Reload the automation from config.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) - - -@bind_hass -def async_reload(hass): - """Reload the automation from config. - - Returns a coroutine object. - """ - return hass.services.async_call(DOMAIN, SERVICE_RELOAD) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the automation.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def trigger_service_handler(service_call): + async def trigger_service_handler(service_call): """Handle automation triggers.""" tasks = [] for entity in component.async_extract_from_service(service_call): tasks.append(entity.async_trigger( - service_call.data.get(ATTR_VARIABLES), True)) + service_call.data.get(ATTR_VARIABLES), + skip_condition=True, + context=service_call.context)) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def turn_onoff_service_handler(service_call): + async def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" tasks = [] method = 'async_{}'.format(service_call.service) @@ -186,10 +142,9 @@ def async_setup(hass, config): tasks.append(getattr(entity, method)()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def toggle_service_handler(service_call): + async def toggle_service_handler(service_call): """Handle automation toggle service calls.""" tasks = [] for entity in component.async_extract_from_service(service_call): @@ -199,15 +154,14 @@ def async_setup(hass, config): tasks.append(entity.async_turn_on()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) - @asyncio.coroutine - def reload_service_handler(service_call): + async def reload_service_handler(service_call): """Remove all automations and load new ones from config.""" - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) hass.services.async_register( DOMAIN, SERVICE_TRIGGER, trigger_service_handler, @@ -272,15 +226,14 @@ class AutomationEntity(ToggleEntity): """Return True if entity is on.""" return self._async_detach_triggers is not None - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Startup with initial state or previous state.""" if self._initial_state is not None: enable_automation = self._initial_state _LOGGER.debug("Automation %s initial state %s from config " "initial_state", self.entity_id, enable_automation) else: - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if state: enable_automation = state.state == STATE_ON self._last_triggered = state.attributes.get('last_triggered') @@ -298,54 +251,50 @@ class AutomationEntity(ToggleEntity): # HomeAssistant is starting up if self.hass.state == CoreState.not_running: - @asyncio.coroutine - def async_enable_automation(event): + async def async_enable_automation(event): """Start automation on startup.""" - yield from self.async_enable() + await self.async_enable() self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, async_enable_automation) # HomeAssistant is running else: - yield from self.async_enable() + await self.async_enable() - @asyncio.coroutine - def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the entity on and update the state.""" if self.is_on: return - yield from self.async_enable() + await self.async_enable() - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" if not self.is_on: return self._async_detach_triggers() self._async_detach_triggers = None - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_trigger(self, variables, skip_condition=False): + async def async_trigger(self, variables, skip_condition=False, + context=None): """Trigger automation. This method is a coroutine. """ if skip_condition or self._cond_func(variables): - yield from self._async_action(self.entity_id, variables) + self.async_set_context(context) + await self._async_action(self.entity_id, variables, context) self._last_triggered = utcnow() - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self): """Remove listeners when removing automation from HASS.""" - yield from self.async_turn_off() + await self.async_turn_off() - @asyncio.coroutine - def async_enable(self): + async def async_enable(self): """Enable this automation entity. This method is a coroutine. @@ -353,9 +302,9 @@ class AutomationEntity(ToggleEntity): if self.is_on: return - self._async_detach_triggers = yield from self._async_attach_triggers( + self._async_detach_triggers = await self._async_attach_triggers( self.async_trigger) - yield from self.async_update_ha_state() + await self.async_update_ha_state() @property def device_state_attributes(self): @@ -368,8 +317,7 @@ class AutomationEntity(ToggleEntity): } -@asyncio.coroutine -def _async_process_config(hass, config, component): +async def _async_process_config(hass, config, component): """Process config and add automations. This method is a coroutine. @@ -411,20 +359,19 @@ def _async_process_config(hass, config, component): entities.append(entity) if entities: - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) def _async_get_action(hass, config, name): """Return an action based on a configuration.""" script_obj = script.Script(hass, config, name) - @asyncio.coroutine - def action(entity_id, variables): + async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) logbook.async_log_entry( hass, name, 'has been triggered', DOMAIN, entity_id) - yield from script_obj.async_run(variables) + await script_obj.async_run(variables, context) return action @@ -448,8 +395,7 @@ def _async_process_if(hass, config, p_config): return if_action -@asyncio.coroutine -def _async_process_trigger(hass, config, trigger_configs, name, action): +async def _async_process_trigger(hass, config, trigger_configs, name, action): """Set up the triggers. This method is a coroutine. @@ -457,13 +403,13 @@ def _async_process_trigger(hass, config, trigger_configs, name, action): removes = [] for conf in trigger_configs: - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, conf.get(CONF_PLATFORM)) if platform is None: return None - remove = yield from platform.async_trigger(hass, conf, action) + remove = await platform.async_trigger(hass, conf, action) if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 7c035d7d1a5..e19a85edae6 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -45,11 +45,11 @@ def async_trigger(hass, config, action): # If event data doesn't match requested schema, skip event return - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'event', 'event': event, }, - }) + }, context=event.context)) return hass.bus.async_listen(event_type, handle_event) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 74cf195bc61..b55d99f706a 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -32,12 +32,12 @@ def async_trigger(hass, config, action): @callback def hass_shutdown(event): """Execute when Home Assistant is shutting down.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'homeassistant', 'event': event, }, - }) + }, context=event.context)) return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown) @@ -45,11 +45,11 @@ def async_trigger(hass, config, action): # Automation are enabled while hass is starting up, fire right away # Check state because a config reload shouldn't trigger it. if hass.state == CoreState.starting: - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'homeassistant', 'event': event, }, - }) + })) return lambda: None diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index b59271f25e5..f0dcbf0be57 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -66,7 +66,7 @@ def async_trigger(hass, config, action): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'numeric_state', 'entity_id': entity, @@ -75,7 +75,7 @@ def async_trigger(hass, config, action): 'from_state': from_s, 'to_state': to_s, } - }) + }, context=to_s.context)) matching = check_numeric_state(entity, from_s, to_s) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 9243f960850..263d4158e25 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -43,7 +43,7 @@ def async_trigger(hass, config, action): @callback def call_action(): """Call action with right context.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'state', 'entity_id': entity, @@ -51,7 +51,7 @@ def async_trigger(hass, config, action): 'to_state': to_s, 'for': time_delta, } - }) + }, context=to_s.context)) # Ignore changes to state attributes if from/to is in use if (not match_all and from_s is not None and to_s is not None and diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 0fcdeaae5e0..67a44f1a347 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -32,13 +32,13 @@ def async_trigger(hass, config, action): @callback def template_listener(entity_id, from_s, to_s): """Listen for state changes and calls action.""" - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'template', 'entity_id': entity_id, 'from_state': from_s, 'to_state': to_s, }, - }) + }, context=to_s.context)) return async_track_template(hass, value_template, template_listener) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 61d846582cb..f30dfe753cb 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -51,7 +51,7 @@ def async_trigger(hass, config, action): # pylint: disable=too-many-boolean-expressions if event == EVENT_ENTER and not from_match and to_match or \ event == EVENT_LEAVE and from_match and not to_match: - hass.async_run_job(action, { + hass.async_run_job(action({ 'trigger': { 'platform': 'zone', 'entity_id': entity, @@ -60,7 +60,7 @@ def async_trigger(hass, config, action): 'zone': zone_state, 'event': event, }, - }) + }, context=to_s.context)) return async_track_state_change(hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 36229828d63..e3b1a941bd2 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -124,7 +124,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice): if not check_control_messages: result['check_control_messages'] = 'OK' else: - result['check_control_messages'] = check_control_messages + cbs_list = [] + for message in check_control_messages: + cbs_list.append(message['ccmDescriptionShort']) + result['check_control_messages'] = cbs_list elif self._attribute == 'charging_status': result['charging_status'] = vehicle_state.charging_status.value # pylint: disable=protected-access diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index d2ca9e7c5e8..b0728ad167c 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -127,6 +127,7 @@ class DeconzBinarySensor(BinarySensorDevice): self._sensor.uniqueid.count(':') != 7): return None serial = self._sensor.uniqueid.split('-', 1)[0] + bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid return { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, @@ -134,4 +135,5 @@ class DeconzBinarySensor(BinarySensorDevice): 'model': self._sensor.modelid, 'name': self._sensor.name, 'sw_version': self._sensor.swversion, + 'via_hub': (DECONZ_DOMAIN, bridgeid), } diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py index dd22a835504..6c8b7ff191e 100644 --- a/homeassistant/components/binary_sensor/homematicip_cloud.py +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -27,17 +27,20 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP Cloud binary sensor from a config entry.""" from homematicip.aio.device import ( - AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector) + AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector, + AsyncWaterSensor, AsyncRotaryHandleSensor) home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: - if isinstance(device, AsyncShutterContact): + if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)): devices.append(HomematicipShutterContact(home, device)) elif isinstance(device, AsyncMotionDetectorIndoor): devices.append(HomematicipMotionDetector(home, device)) elif isinstance(device, AsyncSmokeDetector): devices.append(HomematicipSmokeDetector(home, device)) + elif isinstance(device, AsyncWaterSensor): + devices.append(HomematicipWaterDetector(home, device)) if devices: async_add_entities(devices) @@ -91,3 +94,17 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): def is_on(self): """Return true if smoke is detected.""" return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF + + +class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): + """Representation of a HomematicIP Cloud water detector.""" + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'moisture' + + @property + def is_on(self): + """Return true if moisture or waterlevel is detected.""" + return self._device.moistureDetected or self._device.waterlevelDetected diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 37a26a27214..944cea96b33 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -11,16 +11,20 @@ from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, binary_sensor from homeassistant.components.binary_sensor import ( BinarySensorDevice, DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, + MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -44,13 +48,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT binary sensor.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT binary sensor through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT binary sensor dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT binary sensor.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(binary_sensor.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT binary sensor.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass @@ -68,19 +87,22 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_PAYLOAD_NOT_AVAILABLE), value_template, config.get(CONF_UNIQUE_ID), + discovery_hash, )]) -class MqttBinarySensor(MqttAvailability, BinarySensorDevice): +class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, + BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, name, state_topic, availability_topic, device_class, qos, force_update, payload_on, payload_off, payload_available, payload_not_available, value_template, - unique_id: Optional[str]): + unique_id: Optional[str], discovery_hash): """Initialize the MQTT binary sensor.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._state = None self._state_topic = state_topic @@ -91,11 +113,13 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self._force_update = force_update self._template = value_template self._unique_id = unique_id + self._discovery_hash = discovery_hash @asyncio.coroutine def async_added_to_hass(self): """Subscribe mqtt events.""" - yield from super().async_added_to_hass() + yield from MqttAvailability.async_added_to_hass(self) + yield from MqttDiscoveryUpdate.async_added_to_hass(self) @callback def state_message_received(topic, payload, qos): diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index c60463a8663..7f7278d9789 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -147,6 +147,11 @@ class NestActivityZoneSensor(NestBinarySensor): self.zone = zone self._name = "{} {} activity".format(self._name, self.zone.name) + @property + def unique_id(self): + """Return unique id based on camera serial and zone id.""" + return "{}-{}".format(self.device.serial, self.zone.zone_id) + @property def device_class(self): """Return the device class of the binary sensor.""" diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/binary_sensor/openuv.py index 0b299529a46..c7c27d73ee4 100644 --- a/homeassistant/components/binary_sensor/openuv.py +++ b/homeassistant/components/binary_sensor/openuv.py @@ -7,12 +7,11 @@ https://home-assistant.io/components/binary_sensor.openuv/ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.openuv import ( - BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE, - TYPE_PROTECTION_WINDOW, OpenUvEntity) + BINARY_SENSORS, DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN, + TOPIC_UPDATE, TYPE_PROTECTION_WINDOW, OpenUvEntity) from homeassistant.util.dt import as_local, parse_datetime, utcnow DEPENDENCIES = ['openuv'] @@ -26,17 +25,20 @@ ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv' async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the OpenUV binary sensor platform.""" - if discovery_info is None: - return + """Set up an OpenUV sensor based on existing config.""" + pass - openuv = hass.data[DOMAIN] + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up an OpenUV sensor based on a config entry.""" + openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] binary_sensors = [] - for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + for sensor_type in openuv.binary_sensor_conditions: name, icon = BINARY_SENSORS[sensor_type] binary_sensors.append( - OpenUvBinarySensor(openuv, sensor_type, name, icon)) + OpenUvBinarySensor( + openuv, sensor_type, name, icon, entry.entry_id)) async_add_entities(binary_sensors, True) @@ -44,14 +46,16 @@ async def async_setup_platform( class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): """Define a binary sensor for OpenUV.""" - def __init__(self, openuv, sensor_type, name, icon): + def __init__(self, openuv, sensor_type, name, icon, entry_id): """Initialize the sensor.""" super().__init__(openuv) + self._entry_id = entry_id self._icon = icon self._latitude = openuv.client.latitude self._longitude = openuv.client.longitude self._name = name + self._dispatch_remove = None self._sensor_type = sensor_type self._state = None @@ -83,8 +87,9 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( + self._dispatch_remove = async_dispatcher_connect( self.hass, TOPIC_UPDATE, self._update_data) + self.async_on_remove(self._dispatch_remove) async def async_update(self): """Update the state.""" diff --git a/homeassistant/components/binary_sensor/rachio.py b/homeassistant/components/binary_sensor/rachio.py index 798b6a754d1..36a32c79c5c 100644 --- a/homeassistant/components/binary_sensor/rachio.py +++ b/homeassistant/components/binary_sensor/rachio.py @@ -92,6 +92,11 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Return the name of this sensor including the controller name.""" return "{} online".format(self._controller.name) + @property + def unique_id(self) -> str: + """Return a unique id for this entity.""" + return "{}-online".format(self._controller.controller_id) + @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 412aeb46a3a..ac82ab126fd 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS) import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) @@ -66,13 +67,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): rest = RestData(method, resource, auth, headers, payload, verify_ssl) rest.update() - if rest.data is None: - _LOGGER.error("Unable to fetch REST data from %s", resource) - return False + raise PlatformNotReady + # No need to update the sensor now because it will determine its state + # based in the rest resource that has just been retrieved. add_entities([RestBinarySensor( - hass, rest, name, device_class, value_template)], True) + hass, rest, name, device_class, value_template)]) class RestBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/binary_sensor/ring.py b/homeassistant/components/binary_sensor/ring.py index 102e22cbe2d..3945eb5c926 100644 --- a/homeassistant/components/binary_sensor/ring.py +++ b/homeassistant/components/binary_sensor/ring.py @@ -44,14 +44,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ring = hass.data[DATA_RING] sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - for device in ring.doorbells: + for device in ring.doorbells: # ring.doorbells is doing I/O + for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'doorbell' in SENSOR_TYPES[sensor_type][1]: sensors.append(RingBinarySensor(hass, device, sensor_type)) - for device in ring.stickup_cams: + for device in ring.stickup_cams: # ring.stickup_cams is doing I/O + for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]: sensors.append(RingBinarySensor(hass, device, diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py index 9afd4fe4015..c1be72db374 100644 --- a/homeassistant/components/binary_sensor/spc.py +++ b/homeassistant/components/binary_sensor/spc.py @@ -4,87 +4,66 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.spc/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.spc import ATTR_DISCOVER_DEVICES, DATA_REGISTRY -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback +from homeassistant.components.spc import ( + ATTR_DISCOVER_DEVICES, SIGNAL_UPDATE_SENSOR) _LOGGER = logging.getLogger(__name__) -SPC_TYPE_TO_DEVICE_CLASS = { - '0': 'motion', - '1': 'opening', - '3': 'smoke', -} -SPC_INPUT_TO_SENSOR_STATE = { - '0': STATE_OFF, - '1': STATE_ON, -} +def _get_device_class(zone_type): + from pyspcwebgw.const import ZoneType + return { + ZoneType.ALARM: 'motion', + ZoneType.ENTRY_EXIT: 'opening', + ZoneType.FIRE: 'smoke', + }.get(zone_type) -def _get_device_class(spc_type): - """Get the device class.""" - return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None) - - -def _get_sensor_state(spc_input): - """Get the sensor state.""" - return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE) - - -def _create_sensor(hass, zone): - """Create a SPC sensor.""" - return SpcBinarySensor( - zone_id=zone['id'], name=zone['zone_name'], - state=_get_sensor_state(zone['input']), - device_class=_get_device_class(zone['type']), - spc_registry=hass.data[DATA_REGISTRY]) - - -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the SPC binary sensor.""" if (discovery_info is None or discovery_info[ATTR_DISCOVER_DEVICES] is None): return - async_add_entities( - _create_sensor(hass, zone) - for zone in discovery_info[ATTR_DISCOVER_DEVICES] - if _get_device_class(zone['type'])) + async_add_entities(SpcBinarySensor(zone) + for zone in discovery_info[ATTR_DISCOVER_DEVICES] + if _get_device_class(zone.type)) class SpcBinarySensor(BinarySensorDevice): """Representation of a sensor based on a SPC zone.""" - def __init__(self, zone_id, name, state, device_class, spc_registry): + def __init__(self, zone): """Initialize the sensor device.""" - self._zone_id = zone_id - self._name = name - self._state = state - self._device_class = device_class + self._zone = zone - spc_registry.register_sensor_device(zone_id, self) + async def async_added_to_hass(self): + """Call for adding new entities.""" + async_dispatcher_connect(self.hass, + SIGNAL_UPDATE_SENSOR.format(self._zone.id), + self._update_callback) - @asyncio.coroutine - def async_update_from_spc(self, state, extra): - """Update the state of the device.""" - self._state = state - yield from self.async_update_ha_state() + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) @property def name(self): """Return the name of the device.""" - return self._name + return self._zone.name @property def is_on(self): """Whether the device is switched on.""" - return self._state == STATE_ON + from pyspcwebgw.const import ZoneInput + return self._zone.input == ZoneInput.OPEN @property def hidden(self) -> bool: @@ -100,4 +79,4 @@ class SpcBinarySensor(BinarySensorDevice): @property def device_class(self): """Return the device class.""" - return self._device_class + return _get_device_class(self._zone.type) diff --git a/homeassistant/components/binary_sensor/wirelesstag.py b/homeassistant/components/binary_sensor/wirelesstag.py index 4bec3a824c3..190b408abf3 100644 --- a/homeassistant/components/binary_sensor/wirelesstag.py +++ b/homeassistant/components/binary_sensor/wirelesstag.py @@ -14,9 +14,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.wirelesstag import ( DOMAIN as WIRELESSTAG_DOMAIN, - WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER, - WIRELESSTAG_TYPE_ALSPRO, - WIRELESSTAG_TYPE_WEMO_DEVICE, SIGNAL_BINARY_EVENT_UPDATE, WirelessTagBaseSensor) from homeassistant.const import ( @@ -30,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) # On means in range, Off means out of range SENSOR_PRESENCE = 'presence' -# On means motion detected, Off means cear +# On means motion detected, Off means clear SENSOR_MOTION = 'motion' # On means open, Off means closed @@ -55,49 +52,21 @@ SENSOR_LIGHT = 'light' SENSOR_MOISTURE = 'moisture' # On means tag battery is low, Off means normal -SENSOR_BATTERY = 'low_battery' +SENSOR_BATTERY = 'battery' # Sensor types: Name, device_class, push notification type representing 'on', # attr to check SENSOR_TYPES = { - SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', { - "on": "oor", - "off": "back_in_range" - }, 2], - SENSOR_MOTION: ['Motion', 'motion', 'is_moved', { - "on": "motion_detected", - }, 5], - SENSOR_DOOR: ['Door', 'door', 'is_door_open', { - "on": "door_opened", - "off": "door_closed" - }, 5], - SENSOR_COLD: ['Cold', 'cold', 'is_cold', { - "on": "temp_toolow", - "off": "temp_normal" - }, 4], - SENSOR_HEAT: ['Heat', 'heat', 'is_heat', { - "on": "temp_toohigh", - "off": "temp_normal" - }, 4], - SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', { - "on": "too_dry", - "off": "cap_normal" - }, 2], - SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', { - "on": "too_humid", - "off": "cap_normal" - }, 2], - SENSOR_LIGHT: ['Light', 'light', 'is_light_on', { - "on": "too_bright", - "off": "light_normal" - }, 1], - SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', { - "on": "water_detected", - "off": "water_dried", - }, 1], - SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', { - "on": "low_battery" - }, 3] + SENSOR_PRESENCE: 'Presence', + SENSOR_MOTION: 'Motion', + SENSOR_DOOR: 'Door', + SENSOR_COLD: 'Cold', + SENSOR_HEAT: 'Heat', + SENSOR_DRY: 'Too dry', + SENSOR_WET: 'Too wet', + SENSOR_LIGHT: 'Light', + SENSOR_MOISTURE: 'Leak', + SENSOR_BATTERY: 'Low Battery' } @@ -114,7 +83,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] tags = platform.tags for tag in tags.values(): - allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag) + allowed_sensor_types = tag.supported_binary_events_types for sensor_type in config.get(CONF_MONITORED_CONDITIONS): if sensor_type in allowed_sensor_types: sensors.append(WirelessTagBinarySensor(platform, tag, @@ -127,59 +96,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): """A binary sensor implementation for WirelessTags.""" - @classmethod - def allowed_sensors(cls, tag): - """Return list of allowed sensor types for specific tag type.""" - sensors_map = { - # 13-bit tag - allows everything but not light and moisture - WIRELESSTAG_TYPE_13BIT: [ - SENSOR_PRESENCE, SENSOR_BATTERY, - SENSOR_MOTION, SENSOR_DOOR, - SENSOR_COLD, SENSOR_HEAT, - SENSOR_DRY, SENSOR_WET], - - # Moister/water sensor - temperature and moisture only - WIRELESSTAG_TYPE_WATER: [ - SENSOR_PRESENCE, SENSOR_BATTERY, - SENSOR_COLD, SENSOR_HEAT, - SENSOR_MOISTURE], - - # ALS Pro: allows everything, but not moisture - WIRELESSTAG_TYPE_ALSPRO: [ - SENSOR_PRESENCE, SENSOR_BATTERY, - SENSOR_MOTION, SENSOR_DOOR, - SENSOR_COLD, SENSOR_HEAT, - SENSOR_DRY, SENSOR_WET, - SENSOR_LIGHT], - - # Wemo are power switches. - WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE] - } - - # allow everything if tag type is unknown - # (i just dont have full catalog of them :)) - tag_type = tag.tag_type - fullset = SENSOR_TYPES.keys() - return sensors_map[tag_type] if tag_type in sensors_map else fullset - def __init__(self, api, tag, sensor_type): """Initialize a binary sensor for a Wireless Sensor Tags.""" super().__init__(api, tag) self._sensor_type = sensor_type self._name = '{0} {1}'.format(self._tag.name, - SENSOR_TYPES[self._sensor_type][0]) - self._device_class = SENSOR_TYPES[self._sensor_type][1] - self._tag_attr = SENSOR_TYPES[self._sensor_type][2] - self.binary_spec = SENSOR_TYPES[self._sensor_type][3] - self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4] + self.event.human_readable_name) async def async_added_to_hass(self): """Register callbacks.""" tag_id = self.tag_id event_type = self.device_class + mac = self.tag_manager_mac async_dispatcher_connect( self.hass, - SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type), + SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac), self._on_binary_event_callback) @property @@ -190,7 +121,12 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): @property def device_class(self): """Return the class of the binary sensor.""" - return self._device_class + return self._sensor_type + + @property + def event(self): + """Binary event of tag.""" + return self._tag.event[self._sensor_type] @property def principal_value(self): @@ -198,9 +134,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): Subclasses need override based on type of sensor. """ - return ( - STATE_ON if getattr(self._tag, self._tag_attr, False) - else STATE_OFF) + return STATE_ON if self.event.is_state_on else STATE_OFF def updated_state_value(self): """Use raw princial value.""" @@ -208,7 +142,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): @callback def _on_binary_event_callback(self, event): - """Update state from arrive push notification.""" + """Update state from arrived push notification.""" # state should be 'on' or 'off' self._state = event.data.get('state') self.async_schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 3fb2d7f7f86..1d85d9c9a47 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -15,35 +15,38 @@ from homeassistant.const import CONF_NAME, WEEKDAYS from homeassistant.components.binary_sensor import BinarySensorDevice import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['holidays==0.9.7'] -REQUIREMENTS = ['holidays==0.9.6'] +_LOGGER = logging.getLogger(__name__) # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime -ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', - 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', - 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', - 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany', - 'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland', - 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', - 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', - 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', - 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', - 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', - 'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK', - 'UnitedStates', 'US', 'Wales'] +ALL_COUNTRIES = [ + 'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY' + 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ', + 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR', + 'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU', + 'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', + 'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ', + 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', + 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK', + 'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH', + 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales', +] + +ALLOWED_DAYS = WEEKDAYS + ['holiday'] + CONF_COUNTRY = 'country' CONF_PROVINCE = 'province' CONF_WORKDAYS = 'workdays' +CONF_EXCLUDES = 'excludes' +CONF_OFFSET = 'days_offset' + # By default, Monday - Friday are workdays DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri'] -CONF_EXCLUDES = 'excludes' # By default, public holidays, Saturdays and Sundays are excluded from workdays DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday'] DEFAULT_NAME = 'Workday Sensor' -ALLOWED_DAYS = WEEKDAYS + ['holiday'] -CONF_OFFSET = 'days_offset' DEFAULT_OFFSET = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -86,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: _LOGGER.error("There is no province/state %s in country %s", province, country) - return False + return _LOGGER.debug("Found the following holidays for your configuration:") for date, name in sorted(obj_holidays.items()): diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index cabbbd704a0..aa07a673c97 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -65,28 +65,25 @@ async def _async_setup_iaszone(hass, config, async_add_entities, async def _async_setup_remote(hass, config, async_add_entities, discovery_info): - async def safe(coro): - """Run coro, catching ZigBee delivery errors, and ignoring them.""" - import zigpy.exceptions - try: - await coro - except zigpy.exceptions.DeliveryError as exc: - _LOGGER.warning("Ignoring error during setup: %s", exc) + remote = Remote(**discovery_info) if discovery_info['new_join']: from zigpy.zcl.clusters.general import OnOff, LevelControl out_clusters = discovery_info['out_clusters'] if OnOff.cluster_id in out_clusters: cluster = out_clusters[OnOff.cluster_id] - await safe(cluster.bind()) - await safe(cluster.configure_reporting(0, 0, 600, 1)) + await zha.configure_reporting( + remote.entity_id, cluster, 0, min_report=0, max_report=600, + reportable_change=1 + ) if LevelControl.cluster_id in out_clusters: cluster = out_clusters[LevelControl.cluster_id] - await safe(cluster.bind()) - await safe(cluster.configure_reporting(0, 1, 600, 1)) + await zha.configure_reporting( + remote.entity_id, cluster, 0, min_report=1, max_report=600, + reportable_change=1 + ) - sensor = Switch(**discovery_info) - async_add_entities([sensor], update_before_add=True) + async_add_entities([remote], update_before_add=True) class BinarySensor(zha.Entity, BinarySensorDevice): @@ -131,17 +128,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice): async def async_update(self): """Retrieve latest state.""" - from bellows.types.basic import uint16_t + from zigpy.types.basic import uint16_t result = await zha.safe_read(self._endpoint.ias_zone, ['zone_status'], - allow_cache=False) + allow_cache=False, + only_cache=(not self._initialized)) state = result.get('zone_status', self._state) if isinstance(state, (int, uint16_t)): self._state = result.get('zone_status', self._state) & 3 -class Switch(zha.Entity, BinarySensorDevice): +class Remote(zha.Entity, BinarySensorDevice): """ZHA switch/remote controller/button.""" _domain = DOMAIN diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index 784a96d8615..3bb3a3c79c5 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -9,8 +9,8 @@ import datetime import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_point_in_time from homeassistant.components import zwave -from homeassistant.components.zwave import workaround -from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import +from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import + async_setup_platform, workaround) from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorDevice) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index b0ad1a867a8..dce5961d70d 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['bimmer_connected==0.5.1'] +REQUIREMENTS = ['bimmer_connected==0.5.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 76860702165..b32236e499d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -83,65 +83,71 @@ class Image: content = attr.ib(type=bytes) -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off camera.""" - hass.add_job(async_turn_off, hass, entity_id) - - -@bind_hass -async def async_turn_off(hass, entity_id=None): - """Turn off camera.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on camera.""" - hass.add_job(async_turn_on, hass, entity_id) - - -@bind_hass -async def async_turn_on(hass, entity_id=None): - """Turn on camera, and set operation mode.""" - data = {} - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def enable_motion_detection(hass, entity_id=None): - """Enable Motion Detection.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_ENABLE_MOTION, data)) - - -@bind_hass -def disable_motion_detection(hass, entity_id=None): - """Disable Motion Detection.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_DISABLE_MOTION, data)) - - -@bind_hass -@callback -def async_snapshot(hass, filename, entity_id=None): - """Make a snapshot from a camera.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_FILENAME] = filename - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_SNAPSHOT, data)) - - @bind_hass async def async_get_image(hass, entity_id, timeout=10): """Fetch an image from a camera entity.""" + camera = _get_camera_from_entity_id(hass, entity_id) + + with suppress(asyncio.CancelledError, asyncio.TimeoutError): + with async_timeout.timeout(timeout, loop=hass.loop): + image = await camera.async_camera_image() + + if image: + return Image(camera.content_type, image) + + raise HomeAssistantError('Unable to get image') + + +@bind_hass +async def async_get_mjpeg_stream(hass, request, entity_id): + """Fetch an mjpeg stream from a camera entity.""" + camera = _get_camera_from_entity_id(hass, entity_id) + + return await camera.handle_async_mjpeg_stream(request) + + +async def async_get_still_stream(request, image_cb, content_type, interval): + """Generate an HTTP MJPEG stream from camera images. + + This method must be run in the event loop. + """ + response = web.StreamResponse() + response.content_type = ('multipart/x-mixed-replace; ' + 'boundary=--frameboundary') + await response.prepare(request) + + async def write_to_mjpeg_stream(img_bytes): + """Write image to stream.""" + await response.write(bytes( + '--frameboundary\r\n' + 'Content-Type: {}\r\n' + 'Content-Length: {}\r\n\r\n'.format( + content_type, len(img_bytes)), + 'utf-8') + img_bytes + b'\r\n') + + last_image = None + + while True: + img_bytes = await image_cb() + if not img_bytes: + break + + if img_bytes != last_image: + await write_to_mjpeg_stream(img_bytes) + + # Chrome seems to always ignore first picture, + # print it twice. + if last_image is None: + await write_to_mjpeg_stream(img_bytes) + last_image = img_bytes + + await asyncio.sleep(interval) + + return response + + +def _get_camera_from_entity_id(hass, entity_id): + """Get camera component from entity_id.""" component = hass.data.get(DOMAIN) if component is None: @@ -155,14 +161,7 @@ async def async_get_image(hass, entity_id, timeout=10): if not camera.is_on: raise HomeAssistantError('Camera is off') - with suppress(asyncio.CancelledError, asyncio.TimeoutError): - with async_timeout.timeout(timeout, loop=hass.loop): - image = await camera.async_camera_image() - - if image: - return Image(camera.content_type, image) - - raise HomeAssistantError('Unable to get image') + return camera async def async_setup(hass, config): @@ -290,39 +289,8 @@ class Camera(Entity): This method must be run in the event loop. """ - response = web.StreamResponse() - response.content_type = ('multipart/x-mixed-replace; ' - 'boundary=--frameboundary') - await response.prepare(request) - - async def write_to_mjpeg_stream(img_bytes): - """Write image to stream.""" - await response.write(bytes( - '--frameboundary\r\n' - 'Content-Type: {}\r\n' - 'Content-Length: {}\r\n\r\n'.format( - self.content_type, len(img_bytes)), - 'utf-8') + img_bytes + b'\r\n') - - last_image = None - - while True: - img_bytes = await self.async_camera_image() - if not img_bytes: - break - - if img_bytes and img_bytes != last_image: - await write_to_mjpeg_stream(img_bytes) - - # Chrome seems to always ignore first picture, - # print it twice. - if last_image is None: - await write_to_mjpeg_stream(img_bytes) - last_image = img_bytes - - await asyncio.sleep(interval) - - return response + return await async_get_still_stream(request, self.async_camera_image, + self.content_type, interval) async def handle_async_mjpeg_stream(self, request): """Serve an HTTP MJPEG stream from the camera. diff --git a/homeassistant/components/camera/axis.py b/homeassistant/components/camera/axis.py index 5630759a7f8..4227cca7e2f 100644 --- a/homeassistant/components/camera/axis.py +++ b/homeassistant/components/camera/axis.py @@ -50,7 +50,7 @@ class AxisCamera(MjpegCamera): def __init__(self, hass, config, port): """Initialize Axis Communications camera component.""" - super().__init__(hass, config) + super().__init__(config) self.port = port dispatcher_connect( hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip) diff --git a/homeassistant/components/camera/logi_circle.py b/homeassistant/components/camera/logi_circle.py new file mode 100644 index 00000000000..1dae58ad0f7 --- /dev/null +++ b/homeassistant/components/camera/logi_circle.py @@ -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() diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index ed7d58658ed..f1917aaf23e 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -47,7 +47,7 @@ def async_setup_platform(hass, config, async_add_entities, """Set up a MJPEG IP Camera.""" if discovery_info: config = PLATFORM_SCHEMA(discovery_info) - async_add_entities([MjpegCamera(hass, config)]) + async_add_entities([MjpegCamera(config)]) def extract_image_from_mjpeg(stream): @@ -65,7 +65,7 @@ def extract_image_from_mjpeg(stream): class MjpegCamera(Camera): """An implementation of an IP camera that is reachable over a URL.""" - def __init__(self, hass, device_info): + def __init__(self, device_info): """Initialize a MJPEG camera.""" super().__init__() self._name = device_info.get(CONF_NAME) diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py index cf5c969c650..42ad7d6fa66 100644 --- a/homeassistant/components/camera/mqtt.py +++ b/homeassistant/components/camera/mqtt.py @@ -10,34 +10,53 @@ import logging import voluptuous as vol +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.core import callback -from homeassistant.components import mqtt from homeassistant.const import CONF_NAME +from homeassistant.components import mqtt, camera from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_TOPIC = 'topic' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Camera' DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT Camera.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT camera through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT camera dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT camera.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(camera.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities): + """Set up the MQTT Camera.""" async_add_entities([MqttCamera( config.get(CONF_NAME), + config.get(CONF_UNIQUE_ID), config.get(CONF_TOPIC) )]) @@ -45,11 +64,12 @@ def async_setup_platform(hass, config, async_add_entities, class MqttCamera(Camera): """representation of a MQTT camera.""" - def __init__(self, name, topic): + def __init__(self, name, unique_id, topic): """Initialize the MQTT Camera.""" super().__init__() self._name = name + self._unique_id = unique_id self._topic = topic self._qos = 0 self._last_image = None @@ -64,6 +84,11 @@ class MqttCamera(Camera): """Return the name of this camera.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @asyncio.coroutine def async_added_to_hass(self): """Subscribe MQTT events.""" diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py index e1d26371984..158123989c0 100644 --- a/homeassistant/components/camera/nest.py +++ b/homeassistant/components/camera/nest.py @@ -62,6 +62,23 @@ class NestCamera(Camera): """Return the name of the nest, if any.""" return self._name + @property + def unique_id(self): + """Return the serial number.""" + return self.device.device_id + + @property + def device_info(self): + """Return information about the device.""" + return { + 'identifiers': { + (nest.DOMAIN, self.device.device_id) + }, + 'name': self.device.name_long, + 'manufacturer': 'Nest Labs', + 'model': "Camera", + } + @property def should_poll(self): """Nest camera should poll periodically.""" diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 6c245ffdf43..83d87311646 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -7,17 +7,15 @@ https://www.home-assistant.io/components/camera.proxy/ import asyncio import logging -import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import ( - async_aiohttp_proxy_web, async_get_clientsession) from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util +from . import async_get_still_stream REQUIREMENTS = ['pillow==5.2.0'] @@ -158,22 +156,14 @@ class ProxyCamera(Camera): return self._last_image self._last_image_time = now - url = "{}/api/camera_proxy/{}".format( - self.hass.config.api.base_url, self._proxied_camera) - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10, loop=self.hass.loop): - response = await websession.get(url, headers=self._headers) - image = await response.read() - except asyncio.TimeoutError: - _LOGGER.error("Timeout getting camera image") - return self._last_image - except aiohttp.ClientError as err: - _LOGGER.error("Error getting new camera image: %s", err) + image = await self.hass.components.camera.async_get_image( + self._proxied_camera) + if not image: + _LOGGER.error("Error getting original camera image") return self._last_image image = await self.hass.async_add_job( - _resize_image, image, self._image_opts) + _resize_image, image.content, self._image_opts) if self._cache_images: self._last_image = image @@ -181,56 +171,28 @@ class ProxyCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from camera images.""" - websession = async_get_clientsession(self.hass) - url = "{}/api/camera_proxy_stream/{}".format( - self.hass.config.api.base_url, self._proxied_camera) - stream_coro = websession.get(url, headers=self._headers) - if not self._stream_opts: - return await async_aiohttp_proxy_web( - self.hass, request, stream_coro) + return await self.hass.components.camera.async_get_mjpeg_stream( + request, self._proxied_camera) - response = aiohttp.web.StreamResponse() - response.content_type = ( - 'multipart/x-mixed-replace; boundary=--frameboundary') - await response.prepare(request) - - async def write(img_bytes): - """Write image to stream.""" - await response.write(bytes( - '--frameboundary\r\n' - 'Content-Type: {}\r\n' - 'Content-Length: {}\r\n\r\n'.format( - self.content_type, len(img_bytes)), - 'utf-8') + img_bytes + b'\r\n') - - with async_timeout.timeout(10, loop=self.hass.loop): - req = await stream_coro - - try: - # This would be nicer as an async generator - # But that would only be supported for python >=3.6 - data = b'' - stream = req.content - while True: - chunk = await stream.read(102400) - if not chunk: - break - data += chunk - jpg_start = data.find(b'\xff\xd8') - jpg_end = data.find(b'\xff\xd9') - if jpg_start != -1 and jpg_end != -1: - image = data[jpg_start:jpg_end + 2] - image = await self.hass.async_add_job( - _resize_image, image, self._stream_opts) - await write(image) - data = data[jpg_end + 2:] - finally: - req.close() - - return response + return await async_get_still_stream( + request, self._async_stream_image, + self.content_type, self.frame_interval) @property def name(self): """Return the name of this camera.""" return self._name + + async def _async_stream_image(self): + """Return a still image response from the camera.""" + try: + image = await self.hass.components.camera.async_get_image( + self._proxied_camera) + if not image: + return None + except HomeAssistantError: + raise asyncio.CancelledError + + return await self.hass.async_add_job( + _resize_image, image.content, self._stream_opts) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index 305e29d62d3..c9deca1309d 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -13,8 +13,10 @@ import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ STATE_IDLE, STATE_RECORDING from homeassistant.core import callback -from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST +from homeassistant.components.http.view import KEY_AUTHENTICATED,\ + HomeAssistantView +from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\ + HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util @@ -25,11 +27,13 @@ DEPENDENCIES = ['http'] CONF_BUFFER_SIZE = 'buffer' CONF_IMAGE_FIELD = 'field' +CONF_TOKEN = 'token' DEFAULT_NAME = "Push Camera" ATTR_FILENAME = 'filename' ATTR_LAST_TRIP = 'last_trip' +ATTR_TOKEN = 'token' PUSH_CAMERA_DATA = 'push_camera' @@ -39,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All( cv.time_period, cv.positive_timedelta), vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string, + vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)), }) @@ -50,7 +55,8 @@ async def async_setup_platform(hass, config, async_add_entities, cameras = [PushCamera(config[CONF_NAME], config[CONF_BUFFER_SIZE], - config[CONF_TIMEOUT])] + config[CONF_TIMEOUT], + config.get(CONF_TOKEN))] hass.http.register_view(CameraPushReceiver(hass, config[CONF_IMAGE_FIELD])) @@ -63,6 +69,7 @@ class CameraPushReceiver(HomeAssistantView): url = "/api/camera_push/{entity_id}" name = 'api:camera_push:camera_entity' + requires_auth = False def __init__(self, hass, image_field): """Initialize CameraPushReceiver with camera entity.""" @@ -75,8 +82,21 @@ class CameraPushReceiver(HomeAssistantView): if _camera is None: _LOGGER.error("Unknown %s", entity_id) + status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\ + else HTTP_UNAUTHORIZED return self.json_message('Unknown {}'.format(entity_id), - HTTP_BAD_REQUEST) + status) + + # Supports HA authentication and token based + # when token has been configured + authenticated = (request[KEY_AUTHENTICATED] or + (_camera.token is not None and + request.query.get('token') == _camera.token)) + + if not authenticated: + return self.json_message( + 'Invalid authorization credentials for {}'.format(entity_id), + HTTP_UNAUTHORIZED) try: data = await request.post() @@ -95,7 +115,7 @@ class CameraPushReceiver(HomeAssistantView): class PushCamera(Camera): """The representation of a Push camera.""" - def __init__(self, name, buffer_size, timeout): + def __init__(self, name, buffer_size, timeout, token): """Initialize push camera component.""" super().__init__() self._name = name @@ -106,6 +126,7 @@ class PushCamera(Camera): self._timeout = timeout self.queue = deque([], buffer_size) self._current_image = None + self.token = token async def async_added_to_hass(self): """Call when entity is added to hass.""" @@ -168,5 +189,6 @@ class PushCamera(Camera): name: value for name, value in ( (ATTR_LAST_TRIP, self._last_trip), (ATTR_FILENAME, self._filename), + (ATTR_TOKEN, self.token), ) if value is not None } diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py index f629b501819..d0cb6443fc7 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -39,9 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Ring Door Bell and StickUp Camera.""" ring = hass.data[DATA_RING] @@ -67,14 +65,14 @@ def async_setup_platform(hass, config, async_add_entities, ''' following cameras: {}.'''.format(cameras) _LOGGER.error(err_msg) - hass.components.persistent_notification.async_create( + hass.components.persistent_notification.create( 'Error: {}
' 'You will need to restart hass after fixing.' ''.format(err_msg), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - async_add_entities(cams, True) + add_entities(cams, True) return True diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index b977fcd5c52..1cae5baf1cf 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -63,3 +63,39 @@ onvif_ptz: zoom: description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" example: "ZOOM_IN" + +logi_circle_set_config: + description: Set a configuration property. + fields: + entity_id: + description: Name(s) of entities to apply the operation mode to. + example: "camera.living_room_camera" + mode: + description: "Operation mode. Allowed values: BATTERY_SAVING, LED, PRIVACY_MODE." + example: "PRIVACY_MODE" + value: + description: "Operation value. Allowed values: true, false" + example: true + +logi_circle_livestream_snapshot: + description: Take a snapshot from the camera's livestream. Will wake the camera from sleep if required. + fields: + entity_id: + description: Name(s) of entities to create snapshots from. + example: "camera.living_room_camera" + filename: + description: Template of a Filename. Variable is entity_id. + example: "/tmp/snapshot_{{ entity_id }}.jpg" + +logi_circle_livestream_record: + description: Take a video recording from the camera's livestream. + fields: + entity_id: + description: Name(s) of entities to create recordings from. + example: "camera.living_room_camera" + filename: + description: Template of a Filename. Variable is entity_id. + example: "/tmp/snapshot_{{ entity_id }}.mp4" + duration: + description: Recording duration in seconds. + example: 60 diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py index e48caa42a34..55d8d91d3ee 100644 --- a/homeassistant/components/camera/zoneminder.py +++ b/homeassistant/components/camera/zoneminder.py @@ -4,91 +4,47 @@ Support for ZoneMinder camera streaming. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.zoneminder/ """ -import asyncio import logging -from urllib.parse import urljoin, urlencode from homeassistant.const import CONF_NAME from homeassistant.components.camera.mjpeg import ( CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) - -from homeassistant.components import zoneminder +from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['zoneminder'] -DOMAIN = 'zoneminder' - -# From ZoneMinder's web/includes/config.php.in -ZM_STATE_ALARM = "2" -def _get_image_url(hass, monitor, mode): - zm_data = hass.data[DOMAIN] - query = urlencode({ - 'mode': mode, - 'buffer': monitor['StreamReplayBuffer'], - 'monitor': monitor['Id'], - }) - url = '{zms_url}?{query}'.format( - zms_url=urljoin(zm_data['server_origin'], zm_data['path_zms']), - query=query, - ) - _LOGGER.debug('Monitor %s %s URL (without auth): %s', - monitor['Id'], mode, url) - - if not zm_data['username']: - return url - - url += '&user={:s}'.format(zm_data['username']) - - if not zm_data['password']: - return url - - return url + '&pass={:s}'.format(zm_data['password']) - - -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZoneMinder cameras.""" - cameras = [] - monitors = zoneminder.get_state('api/monitors.json') + zm_client = hass.data[ZONEMINDER_DOMAIN] + + monitors = zm_client.get_monitors() if not monitors: _LOGGER.warning("Could not fetch monitors from ZoneMinder") return - for i in monitors['monitors']: - monitor = i['Monitor'] - - if monitor['Function'] == 'None': - _LOGGER.info("Skipping camera %s", monitor['Id']) - continue - - _LOGGER.info("Initializing camera %s", monitor['Id']) - - device_info = { - CONF_NAME: monitor['Name'], - CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'), - CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single') - } - cameras.append(ZoneMinderCamera(hass, device_info, monitor)) - - if not cameras: - _LOGGER.warning("No active cameras found") - return - - async_add_entities(cameras) + cameras = [] + for monitor in monitors: + _LOGGER.info("Initializing camera %s", monitor.id) + cameras.append(ZoneMinderCamera(monitor)) + add_entities(cameras) class ZoneMinderCamera(MjpegCamera): """Representation of a ZoneMinder Monitor Stream.""" - def __init__(self, hass, device_info, monitor): + def __init__(self, monitor): """Initialize as a subclass of MjpegCamera.""" - super().__init__(hass, device_info) - self._monitor_id = int(monitor['Id']) + device_info = { + CONF_NAME: monitor.name, + CONF_MJPEG_URL: monitor.mjpeg_image_url, + CONF_STILL_IMAGE_URL: monitor.still_image_url + } + super().__init__(device_info) self._is_recording = None + self._monitor = monitor @property def should_poll(self): @@ -97,17 +53,8 @@ class ZoneMinderCamera(MjpegCamera): def update(self): """Update our recording state from the ZM API.""" - _LOGGER.debug("Updating camera state for monitor %i", self._monitor_id) - status_response = zoneminder.get_state( - 'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id - ) - - if not status_response: - _LOGGER.warning("Could not get status for monitor %i", - self._monitor_id) - return - - self._is_recording = status_response.get('status') == ZM_STATE_ALARM + _LOGGER.debug("Updating camera state for monitor %i", self._monitor.id) + self._is_recording = self._monitor.is_recording @property def is_recording(self): diff --git a/homeassistant/components/cast/.translations/ca.json b/homeassistant/components/cast/.translations/ca.json index e65e00f8624..570cc7fdc00 100644 --- a/homeassistant/components/cast/.translations/ca.json +++ b/homeassistant/components/cast/.translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.", - "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Google Cast." + "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Google Cast." }, "step": { "confirm": { diff --git a/homeassistant/components/cast/.translations/de.json b/homeassistant/components/cast/.translations/de.json index a37dbd6f5b7..ac1ebbeb236 100644 --- a/homeassistant/components/cast/.translations/de.json +++ b/homeassistant/components/cast/.translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie Google Cast einrichten?", + "description": "M\u00f6chtest du Google Cast einrichten?", "title": "Google Cast" } }, diff --git a/homeassistant/components/cast/.translations/fr.json b/homeassistant/components/cast/.translations/fr.json index acacddf2187..99feeb3c898 100644 --- a/homeassistant/components/cast/.translations/fr.json +++ b/homeassistant/components/cast/.translations/fr.json @@ -2,12 +2,14 @@ "config": { "abort": { "no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.", - "single_instance_allowed": "Seulement une seule configuration de Google Cast est n\u00e9cessaire." + "single_instance_allowed": "Une seule configuration de Google Cast est n\u00e9cessaire." }, "step": { "confirm": { - "description": "Voulez-vous configurer Google Cast?" + "description": "Voulez-vous configurer Google Cast?", + "title": "Google Cast" } - } + }, + "title": "Google Cast" } } \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/id.json b/homeassistant/components/cast/.translations/id.json new file mode 100644 index 00000000000..86fb32c0844 --- /dev/null +++ b/homeassistant/components/cast/.translations/id.json @@ -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" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/nn.json b/homeassistant/components/cast/.translations/nn.json new file mode 100644 index 00000000000..7f550155658 --- /dev/null +++ b/homeassistant/components/cast/.translations/nn.json @@ -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" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 6885f24269a..53f5e704019 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -35,4 +35,5 @@ async def _async_has_devices(hass): config_entry_flow.register_discovery_flow( - DOMAIN, 'Google Cast', _async_has_devices) + DOMAIN, 'Google Cast', _async_has_devices, + config_entries.CONN_CLASS_LOCAL_PUSH) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index a3273f67cc2..98483c454bc 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -10,7 +10,6 @@ import functools as ft import voluptuous as vol -from homeassistant.loader import bind_hass from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.entity_component import EntityComponent @@ -20,7 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, - PRECISION_TENTHS, ) + PRECISION_TENTHS) DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 @@ -142,107 +141,6 @@ SET_SWING_MODE_SCHEMA = vol.Schema({ }) -@bind_hass -def set_away_mode(hass, away_mode, entity_id=None): - """Turn all or specified climate devices away mode on.""" - data = { - ATTR_AWAY_MODE: away_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) - - -@bind_hass -def set_hold_mode(hass, hold_mode, entity_id=None): - """Set new hold mode.""" - data = { - ATTR_HOLD_MODE: hold_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data) - - -@bind_hass -def set_aux_heat(hass, aux_heat, entity_id=None): - """Turn all or specified climate devices auxiliary heater on.""" - data = { - ATTR_AUX_HEAT: aux_heat - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) - - -@bind_hass -def set_temperature(hass, temperature=None, entity_id=None, - target_temp_high=None, target_temp_low=None, - operation_mode=None): - """Set new target temperature.""" - kwargs = { - key: value for key, value in [ - (ATTR_TEMPERATURE, temperature), - (ATTR_TARGET_TEMP_HIGH, target_temp_high), - (ATTR_TARGET_TEMP_LOW, target_temp_low), - (ATTR_ENTITY_ID, entity_id), - (ATTR_OPERATION_MODE, operation_mode) - ] if value is not None - } - _LOGGER.debug("set_temperature start data=%s", kwargs) - hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) - - -@bind_hass -def set_humidity(hass, humidity, entity_id=None): - """Set new target humidity.""" - data = {ATTR_HUMIDITY: humidity} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) - - -@bind_hass -def set_fan_mode(hass, fan, entity_id=None): - """Set all or specified climate devices fan mode on.""" - data = {ATTR_FAN_MODE: fan} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) - - -@bind_hass -def set_operation_mode(hass, operation_mode, entity_id=None): - """Set new target operation mode.""" - data = {ATTR_OPERATION_MODE: operation_mode} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) - - -@bind_hass -def set_swing_mode(hass, swing_mode, entity_id=None): - """Set new target swing mode.""" - data = {ATTR_SWING_MODE: swing_mode} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) - - async def async_setup(hass, config): """Set up climate devices.""" component = hass.data[DOMAIN] = \ diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/climate/econet.py index 9350b8f853d..8be640c37e1 100644 --- a/homeassistant/components/climate/econet.py +++ b/homeassistant/components/climate/econet.py @@ -18,7 +18,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyeconet==0.0.5'] +REQUIREMENTS = ['pyeconet==0.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/evohome.py b/homeassistant/components/climate/evohome.py new file mode 100644 index 00000000000..f0631228fd8 --- /dev/null +++ b/homeassistant/components/climate/evohome.py @@ -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 + "]" + ) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index fec18329878..85879b8122a 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -251,6 +251,14 @@ class GenericThermostat(ClimateDevice): # Ensure we update the current operation after changing the mode self.schedule_update_ha_state() + async def async_turn_on(self): + """Turn thermostat on.""" + await self.async_set_operation_mode(self.operation_list[0]) + + async def async_turn_off(self): + """Turn thermostat off.""" + await self.async_set_operation_mode(STATE_OFF) + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 6d54695fa7a..c445a495073 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_REGION) -REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2'] +REQUIREMENTS = ['evohomeclient==0.2.7', 'somecomfort==0.5.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 9e227e002b5..23def7c4b87 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, climate from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, @@ -21,9 +21,13 @@ from homeassistant.components.climate import ( from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) @@ -126,13 +130,28 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT climate devices.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT climate device through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT climate device dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT climate device.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(climate.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT climate devices.""" template_keys = ( CONF_POWER_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, @@ -194,11 +213,12 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_MIN_TEMP), - config.get(CONF_MAX_TEMP)) - ]) + config.get(CONF_MAX_TEMP), + discovery_hash, + )]) -class MqttClimate(MqttAvailability, ClimateDevice): +class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): """Representation of an MQTT climate device.""" def __init__(self, hass, name, topic, value_templates, qos, retain, @@ -207,10 +227,11 @@ class MqttClimate(MqttAvailability, ClimateDevice): current_swing_mode, current_operation, aux, send_if_off, payload_on, payload_off, availability_topic, payload_available, payload_not_available, - min_temp, max_temp): + min_temp, max_temp, discovery_hash): """Initialize the climate device.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self.hass = hass self._name = name self._topic = topic @@ -235,11 +256,13 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._payload_off = payload_off self._min_temp = min_temp self._max_temp = max_temp + self._discovery_hash = discovery_hash @asyncio.coroutine def async_added_to_hass(self): """Handle being added to home assistant.""" - yield from super().async_added_to_hass() + yield from MqttAvailability.async_added_to_hass(self) + yield from MqttDiscoveryUpdate.async_added_to_hass(self) @callback def handle_current_temp_received(topic, payload, qos): diff --git a/homeassistant/components/climate/opentherm_gw.py b/homeassistant/components/climate/opentherm_gw.py new file mode 100644 index 00000000000..00049d26b7f --- /dev/null +++ b/homeassistant/components/climate/opentherm_gw.py @@ -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 diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 429b544aefc..14cd2a0f02e 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -174,8 +174,8 @@ class RadioThermostat(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" return { - ATTR_FAN: self._fmode, - ATTR_MODE: self._tmode, + ATTR_FAN: self._fstate, + ATTR_MODE: self._tstate, } @property diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index d8e6843bec8..3013a155380 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -118,7 +118,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): self.hass, self.target_temperature_low, self.temperature_unit, PRECISION_TENTHS) - if self.external_temperature: + if self.external_temperature is not None: data[ATTR_EXTERNAL_TEMPERATURE] = show_temp( self.hass, self.external_temperature, self.temperature_unit, PRECISION_TENTHS) @@ -126,16 +126,16 @@ class WinkThermostat(WinkDevice, ClimateDevice): if self.smart_temperature: data[ATTR_SMART_TEMPERATURE] = self.smart_temperature - if self.occupied: + if self.occupied is not None: data[ATTR_OCCUPIED] = self.occupied - if self.eco_target: + if self.eco_target is not None: data[ATTR_ECO_TARGET] = self.eco_target - if self.heat_on: + if self.heat_on is not None: data[ATTR_HEAT_ON] = self.heat_on - if self.cool_on: + if self.cool_on is not None: data[ATTR_COOL_ON] = self.cool_on current_humidity = self.current_humidity diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index f87f2e83f5d..77b5e111686 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -10,8 +10,8 @@ from homeassistant.components.climate import ( DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) -from homeassistant.components.zwave import ZWaveDeviceEntity -from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import +from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import + ZWaveDeviceEntity, async_setup_platform) from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 8c1a9751c19..33a939bf9d0 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -23,12 +23,17 @@ from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.google_assistant import helpers as ga_h from homeassistant.components.google_assistant import const as ga_c -from . import http_api, iot +from . import http_api, iot, auth_api from .const import CONFIG_DIR, DOMAIN, SERVERS REQUIREMENTS = ['warrant==0.6.1'] +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 +STORAGE_ENABLE_ALEXA = 'alexa_enabled' +STORAGE_ENABLE_GOOGLE = 'google_enabled' _LOGGER = logging.getLogger(__name__) +_UNDEF = object() CONF_ALEXA = 'alexa' CONF_ALIASES = 'aliases' @@ -39,6 +44,7 @@ CONF_GOOGLE_ACTIONS = 'google_actions' CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url' +CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url' DEFAULT_MODE = 'production' DEPENDENCIES = ['http'] @@ -79,6 +85,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_REGION): str, vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str, + vol.Optional(CONF_SUBSCRIPTION_INFO_URL): str, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, }), @@ -114,18 +121,21 @@ class Cloud: def __init__(self, hass, mode, alexa, google_actions, cognito_client_id=None, user_pool_id=None, region=None, - relayer=None, google_actions_sync_url=None): + relayer=None, google_actions_sync_url=None, + subscription_info_url=None): """Create an instance of Cloud.""" self.hass = hass self.mode = mode self.alexa_config = alexa self._google_actions = google_actions self._gactions_config = None + self._prefs = None self.jwt_keyset = None self.id_token = None self.access_token = None self.refresh_token = None self.iot = iot.CloudIoT(self) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) if mode == MODE_DEV: self.cognito_client_id = cognito_client_id @@ -133,6 +143,7 @@ class Cloud: self.region = region self.relayer = relayer self.google_actions_sync_url = google_actions_sync_url + self.subscription_info_url = subscription_info_url else: info = SERVERS[mode] @@ -142,6 +153,7 @@ class Cloud: self.region = info['region'] self.relayer = info['relayer'] self.google_actions_sync_url = info['google_actions_sync_url'] + self.subscription_info_url = info['subscription_info_url'] @property def is_logged_in(self): @@ -188,6 +200,16 @@ class Cloud: return self._gactions_config + @property + def alexa_enabled(self): + """Return if Alexa is enabled.""" + return self._prefs[STORAGE_ENABLE_ALEXA] + + @property + def google_enabled(self): + """Return if Google is enabled.""" + return self._prefs[STORAGE_ENABLE_GOOGLE] + def path(self, *parts): """Get config path inside cloud dir. @@ -195,6 +217,15 @@ class Cloud: """ return self.hass.config.path(CONFIG_DIR, *parts) + async def fetch_subscription_info(self): + """Fetch subscription info.""" + await self.hass.async_add_executor_job(auth_api.check_token, self) + websession = self.hass.helpers.aiohttp_client.async_get_clientsession() + return await websession.get( + self.subscription_info_url, headers={ + 'authorization': self.id_token + }) + @asyncio.coroutine def logout(self): """Close connection and remove all credentials.""" @@ -217,10 +248,23 @@ class Cloud: 'refresh_token': self.refresh_token, }, indent=4)) - @asyncio.coroutine - def async_start(self, _): + async def async_start(self, _): """Start the cloud component.""" - success = yield from self._fetch_jwt_keyset() + prefs = await self._store.async_load() + if prefs is None: + prefs = {} + if self.mode not in prefs: + # Default to True if already logged in to make this not a + # breaking change. + enabled = await self.hass.async_add_executor_job( + os.path.isfile, self.user_info_path) + prefs = { + STORAGE_ENABLE_ALEXA: enabled, + STORAGE_ENABLE_GOOGLE: enabled, + } + self._prefs = prefs + + success = await self._fetch_jwt_keyset() # Fetching keyset can fail if internet is not up yet. if not success: @@ -241,7 +285,7 @@ class Cloud: with open(user_info, 'rt') as file: return json.loads(file.read()) - info = yield from self.hass.async_add_job(load_config) + info = await self.hass.async_add_job(load_config) if info is None: return @@ -260,6 +304,15 @@ class Cloud: self.hass.add_job(self.iot.connect()) + async def update_preferences(self, *, google_enabled=_UNDEF, + alexa_enabled=_UNDEF): + """Update user preferences.""" + if google_enabled is not _UNDEF: + self._prefs[STORAGE_ENABLE_GOOGLE] = google_enabled + if alexa_enabled is not _UNDEF: + self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled + await self._store.async_save(self._prefs) + @asyncio.coroutine def _fetch_jwt_keyset(self): """Fetch the JWT keyset for the Cognito instance.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 82128206d47..88fb88474a1 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -11,6 +11,8 @@ SERVERS = { 'relayer': 'wss://cloud.hass.io:8000/websocket', 'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.' 'amazonaws.com/prod/smart_home_sync'), + 'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/' + 'subscription_info') } } diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index a4b3b59f333..c81ec38bace 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -6,22 +6,56 @@ import logging import async_timeout import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import ( RequestDataValidator) +from homeassistant.components import websocket_api from . import auth_api from .const import DOMAIN, REQUEST_TIMEOUT +from .iot import STATE_DISCONNECTED _LOGGER = logging.getLogger(__name__) +WS_TYPE_STATUS = 'cloud/status' +SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_STATUS, +}) + + +WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs' +SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_UPDATE_PREFS, + vol.Optional('google_enabled'): bool, + vol.Optional('alexa_enabled'): bool, +}) + + +WS_TYPE_SUBSCRIPTION = 'cloud/subscription' +SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_SUBSCRIPTION, +}) + + async def async_setup(hass): """Initialize the HTTP API.""" + hass.components.websocket_api.async_register_command( + WS_TYPE_STATUS, websocket_cloud_status, + SCHEMA_WS_STATUS + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_SUBSCRIPTION, websocket_subscription, + SCHEMA_WS_SUBSCRIPTION + ) + hass.components.websocket_api.async_register_command( + WS_TYPE_UPDATE_PREFS, websocket_update_prefs, + SCHEMA_WS_UPDATE_PREFS + ) hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLogoutView) - hass.http.register_view(CloudAccountView) hass.http.register_view(CloudRegisterView) hass.http.register_view(CloudResendConfirmView) hass.http.register_view(CloudForgotPasswordView) @@ -102,9 +136,7 @@ class CloudLoginView(HomeAssistantView): data['password']) hass.async_add_job(cloud.iot.connect) - # Allow cloud to start connecting. - await asyncio.sleep(0, loop=hass.loop) - return self.json(_account_data(cloud)) + return self.json({'success': True}) class CloudLogoutView(HomeAssistantView): @@ -125,23 +157,6 @@ class CloudLogoutView(HomeAssistantView): return self.json_message('ok') -class CloudAccountView(HomeAssistantView): - """View to retrieve account info.""" - - url = '/api/cloud/account' - name = 'api:cloud:account' - - async def get(self, request): - """Get account info.""" - hass = request.app['hass'] - cloud = hass.data[DOMAIN] - - if not cloud.is_logged_in: - return self.json_message('Not logged in', 400) - - return self.json(_account_data(cloud)) - - class CloudRegisterView(HomeAssistantView): """Register on the Home Assistant cloud.""" @@ -209,12 +224,73 @@ class CloudForgotPasswordView(HomeAssistantView): return self.json_message('ok') +@callback +def websocket_cloud_status(hass, connection, msg): + """Handle request for account info. + + Async friendly. + """ + cloud = hass.data[DOMAIN] + connection.to_write.put_nowait( + websocket_api.result_message(msg['id'], _account_data(cloud))) + + +@websocket_api.async_response +async def websocket_subscription(hass, connection, msg): + """Handle request for account info.""" + cloud = hass.data[DOMAIN] + + if not cloud.is_logged_in: + connection.to_write.put_nowait(websocket_api.error_message( + msg['id'], 'not_logged_in', + 'You need to be logged in to the cloud.')) + return + + with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + response = await cloud.fetch_subscription_info() + + if response.status == 200: + connection.send_message_outside(websocket_api.result_message( + msg['id'], await response.json())) + else: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'request_failed', 'Failed to request subscription')) + + +@websocket_api.async_response +async def websocket_update_prefs(hass, connection, msg): + """Handle request for account info.""" + cloud = hass.data[DOMAIN] + + if not cloud.is_logged_in: + connection.to_write.put_nowait(websocket_api.error_message( + msg['id'], 'not_logged_in', + 'You need to be logged in to the cloud.')) + return + + changes = dict(msg) + changes.pop('id') + changes.pop('type') + await cloud.update_preferences(**changes) + + connection.send_message_outside(websocket_api.result_message( + msg['id'], {'success': True})) + + def _account_data(cloud): """Generate the auth data JSON response.""" + if not cloud.is_logged_in: + return { + 'logged_in': False, + 'cloud': STATE_DISCONNECTED, + } + claims = cloud.claims return { + 'logged_in': True, 'email': claims['email'], - 'sub_exp': claims['custom:sub-exp'], 'cloud': cloud.iot.state, + 'google_enabled': cloud.google_enabled, + 'alexa_enabled': cloud.alexa_enabled, } diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index f4ce7bb3d1a..fd525ed33a8 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -227,6 +227,9 @@ def async_handle_message(hass, cloud, handler_name, payload): @asyncio.coroutine def async_handle_alexa(hass, cloud, payload): """Handle an incoming IoT message for Alexa.""" + if not cloud.alexa_enabled: + return alexa.turned_off_response(payload) + result = yield from alexa.async_handle_message( hass, cloud.alexa_config, payload) return result @@ -236,6 +239,9 @@ def async_handle_alexa(hass, cloud, payload): @asyncio.coroutine def async_handle_google_actions(hass, cloud, payload): """Handle an incoming IoT message for Google Actions.""" + if not cloud.google_enabled: + return ga.turned_off_response(payload) + result = yield from ga.async_handle_message( hass, cloud.gactions_config, payload) return result diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 581d8fc3f7b..df0e2f13ac1 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -13,8 +13,17 @@ from homeassistant.util.yaml import load_yaml, dump DOMAIN = 'config' DEPENDENCIES = ['http'] -SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script', - 'entity_registry', 'config_entries') +SECTIONS = ( + 'automation', + 'config_entries', + 'core', + 'customize', + 'device_registry', + 'entity_registry', + 'group', + 'hassbian', + 'script', +) ON_DEMAND = ('zwave',) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 223159eb415..2d13ac07025 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -3,10 +3,9 @@ import asyncio from collections import OrderedDict import uuid -from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView -from homeassistant.components.automation import ( - PLATFORM_SCHEMA, DOMAIN, async_reload) +from homeassistant.const import CONF_ID, SERVICE_RELOAD +from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv @@ -16,9 +15,13 @@ CONFIG_PATH = 'automations.yaml' @asyncio.coroutine def async_setup(hass): """Set up the Automation config API.""" + async def hook(hass): + """post_write_hook for Config View that reloads automations.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + hass.http.register_view(EditAutomationConfigView( DOMAIN, 'config', CONFIG_PATH, cv.string, - PLATFORM_SCHEMA, post_write_hook=async_reload + PLATFORM_SCHEMA, post_write_hook=hook )) return True diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 04d2c713cdc..73b2767be4b 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -7,9 +7,6 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView) -REQUIREMENTS = ['voluptuous-serialize==2.0.0'] - - @asyncio.coroutine def async_setup(hass): """Enable the Home Assistant views.""" @@ -57,6 +54,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView): 'title': entry.title, 'source': entry.source, 'state': entry.state, + 'connection_class': entry.connection_class, } for entry in hass.config_entries.async_entries()]) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py new file mode 100644 index 00000000000..88aa5727a97 --- /dev/null +++ b/homeassistant/components/config/device_registry.py @@ -0,0 +1,47 @@ +"""HTTP views to interact with the device registry.""" +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.components import websocket_api + +DEPENDENCIES = ['websocket_api'] + +WS_TYPE_LIST = 'config/device_registry/list' +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_LIST, +}) + + +async def async_setup(hass): + """Enable the Entity Registry views.""" + hass.components.websocket_api.async_register_command( + WS_TYPE_LIST, websocket_list_devices, + SCHEMA_WS_LIST + ) + return True + + +@callback +def websocket_list_devices(hass, connection, msg): + """Handle list devices command. + + Async friendly. + """ + async def retrieve_entities(): + """Get devices from registry.""" + registry = await async_get_registry(hass) + connection.send_message_outside(websocket_api.result_message( + msg['id'], [{ + 'config_entries': list(entry.config_entries), + 'connections': list(entry.connections), + 'manufacturer': entry.manufacturer, + 'model': entry.model, + 'name': entry.name, + 'sw_version': entry.sw_version, + 'id': entry.id, + 'hub_device_id': entry.hub_device_id, + } for entry in registry.devices.values()] + )) + + hass.async_add_job(retrieve_entities()) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 2fac420c39c..0f9abf167e5 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -8,6 +8,11 @@ from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['websocket_api'] +WS_TYPE_LIST = 'config/entity_registry/list' +SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_LIST, +}) + WS_TYPE_GET = 'config/entity_registry/get' SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET, @@ -26,6 +31,10 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ async def async_setup(hass): """Enable the Entity Registry views.""" + hass.components.websocket_api.async_register_command( + WS_TYPE_LIST, websocket_list_entities, + SCHEMA_WS_LIST + ) hass.components.websocket_api.async_register_command( WS_TYPE_GET, websocket_get_entity, SCHEMA_WS_GET @@ -37,6 +46,29 @@ async def async_setup(hass): return True +@callback +def websocket_list_entities(hass, connection, msg): + """Handle list registry entries command. + + Async friendly. + """ + async def retrieve_entities(): + """Get entities from registry.""" + registry = await async_get_registry(hass) + connection.send_message_outside(websocket_api.result_message( + msg['id'], [{ + 'config_entry_id': entry.config_entry_id, + 'device_id': entry.device_id, + 'disabled_by': entry.disabled_by, + 'entity_id': entry.entity_id, + 'name': entry.name, + 'platform': entry.platform, + } for entry in registry.entities.values()] + )) + + hass.async_add_job(retrieve_entities()) + + @callback def websocket_get_entity(hass, connection, msg): """Handle get entity registry entry command. diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 345c8e4a849..6f2eaa3ff67 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -2,7 +2,8 @@ import asyncio from homeassistant.components.config import EditKeyBasedConfigView -from homeassistant.components.script import SCRIPT_ENTRY_SCHEMA, async_reload +from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv @@ -12,8 +13,12 @@ CONFIG_PATH = 'scripts.yaml' @asyncio.coroutine def async_setup(hass): """Set up the script config API.""" + async def hook(hass): + """post_write_hook for Config View that reloads scripts.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + hass.http.register_view(EditKeyBasedConfigView( 'script', 'config', CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA, - post_write_hook=async_reload + post_write_hook=hook )) return True diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index d720819a0ab..9ef4d4374ce 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -9,12 +9,10 @@ import logging import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -51,48 +49,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@bind_hass -def increment(hass, entity_id): - """Increment a counter.""" - hass.add_job(async_increment, hass, entity_id) - - -@callback -@bind_hass -def async_increment(hass, entity_id): - """Increment a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def decrement(hass, entity_id): - """Decrement a counter.""" - hass.add_job(async_decrement, hass, entity_id) - - -@callback -@bind_hass -def async_decrement(hass, entity_id): - """Decrement a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def reset(hass, entity_id): - """Reset a counter.""" - hass.add_job(async_reset, hass, entity_id) - - -@callback -@bind_hass -def async_reset(hass, entity_id): - """Reset a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) - - async def async_setup(hass, config): """Set up the counters.""" component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 05c5e46e44e..ec11b139f6b 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -35,8 +35,9 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers') ENTITY_ID_FORMAT = DOMAIN + '.{}' DEVICE_CLASSES = [ - 'window', # Window control + 'damper', 'garage', # Garage door control + 'window', # Window control ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) @@ -80,67 +81,9 @@ def is_closed(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_CLOSED) -@bind_hass -def open_cover(hass, entity_id=None): - """Open all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_OPEN_COVER, data) - - -@bind_hass -def close_cover(hass, entity_id=None): - """Close all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, data) - - -@bind_hass -def set_cover_position(hass, position, entity_id=None): - """Move to specific position all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_POSITION] = position - hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, data) - - -@bind_hass -def stop_cover(hass, entity_id=None): - """Stop all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP_COVER, data) - - -@bind_hass -def open_cover_tilt(hass, entity_id=None): - """Open all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_OPEN_COVER_TILT, data) - - -@bind_hass -def close_cover_tilt(hass, entity_id=None): - """Close all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLOSE_COVER_TILT, data) - - -@bind_hass -def set_cover_tilt_position(hass, tilt_position, entity_id=None): - """Move to specific tilt position all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_TILT_POSITION] = tilt_position - hass.services.call(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data) - - -@bind_hass -def stop_cover_tilt(hass, entity_id=None): - """Stop all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data) - - async def async_setup(hass, config): """Track states and offer events for covers.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS) await component.async_setup(config) @@ -195,6 +138,16 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class CoverDevice(Entity): """Representation a cover.""" diff --git a/homeassistant/components/cover/deconz.py b/homeassistant/components/cover/deconz.py new file mode 100644 index 00000000000..9fe65596336 --- /dev/null +++ b/homeassistant/components/cover/deconz.py @@ -0,0 +1,146 @@ +""" +Support for deCONZ covers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.deconz/ +""" +from homeassistant.components.deconz.const import ( + COVER_TYPES, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, + DECONZ_DOMAIN) +from homeassistant.components.cover import ( + ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, + SUPPORT_SET_POSITION) +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['deconz'] + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Unsupported way of setting up deCONZ covers.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up covers for deCONZ component. + + Covers are based on same device class as lights in deCONZ. + """ + @callback + def async_add_cover(lights): + """Add cover from deCONZ.""" + entities = [] + for light in lights: + if light.type in COVER_TYPES: + entities.append(DeconzCover(light)) + async_add_entities(entities, True) + + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover)) + + async_add_cover(hass.data[DATA_DECONZ].lights.values()) + + +class DeconzCover(CoverDevice): + """Representation of a deCONZ cover.""" + + def __init__(self, cover): + """Set up cover and add update callback to get data from websocket.""" + self._cover = cover + self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + async def async_added_to_hass(self): + """Subscribe to covers events.""" + self._cover.register_async_callback(self.async_update_callback) + self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._cover.deconz_id + + async def async_will_remove_from_hass(self) -> None: + """Disconnect cover object when removed.""" + self._cover.remove_callback(self.async_update_callback) + self._cover = None + + @callback + def async_update_callback(self, reason): + """Update the cover's state.""" + self.async_schedule_update_ha_state() + + @property + def current_cover_position(self): + """Return the current position of the cover.""" + if self.is_closed: + return 0 + return int(self._cover.brightness / 255 * 100) + + @property + def is_closed(self): + """Return if the cover is closed.""" + return not self._cover.state + + @property + def name(self): + """Return the name of the cover.""" + return self._cover.name + + @property + def unique_id(self): + """Return a unique identifier for this cover.""" + return self._cover.uniqueid + + @property + def device_class(self): + """Return the class of the cover.""" + return 'damper' + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + @property + def available(self): + """Return True if light is available.""" + return self._cover.reachable + + @property + def should_poll(self): + """No polling needed.""" + return False + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + data = {'on': False} + if position > 0: + data['on'] = True + data['bri'] = int(position / 100 * 255) + await self._cover.async_set_state(data) + + async def async_open_cover(self, **kwargs): + """Open cover.""" + data = {ATTR_POSITION: 100} + await self.async_set_cover_position(**data) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + data = {ATTR_POSITION: 0} + await self.async_set_cover_position(**data) + + @property + def device_info(self): + """Return a device description for device registry.""" + if (self._cover.uniqueid is None or + self._cover.uniqueid.count(':') != 7): + return None + serial = self._cover.uniqueid.split('-', 1)[0] + bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid + return { + 'connections': {(CONNECTION_ZIGBEE, serial)}, + 'identifiers': {(DECONZ_DOMAIN, serial)}, + 'manufacturer': self._cover.manufacturer, + 'model': self._cover.modelid, + 'name': self._cover.name, + 'sw_version': self._cover.swversion, + 'via_hub': (DECONZ_DOMAIN, bridgeid), + } diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 428c1f326e4..4ead61e6b7a 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -44,6 +44,8 @@ class ISYCoverDevice(ISYDevice, CoverDevice): @property def current_cover_position(self) -> int: """Return the current cover position.""" + if self.is_unknown() or self.value is None: + return None return sorted((0, self.value, 100))[1] @property diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 977353cb318..cbc8fbee274 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -5,11 +5,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.mqtt/ """ import logging +from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, cover from homeassistant.components.cover import ( CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, @@ -20,10 +21,14 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - valid_publish_topic, valid_subscribe_topic, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic, + MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -45,6 +50,7 @@ CONF_TILT_MIN = 'tilt_min' CONF_TILT_MAX = 'tilt_max' CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' CONF_TILT_INVERT_STATE = 'tilt_invert_state' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' @@ -89,15 +95,32 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ default=DEFAULT_TILT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_INVERT_STATE, default=DEFAULT_TILT_INVERT_STATE): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT Cover.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT cover through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT cover dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add an MQTT cover.""" + 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(cover.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT Cover.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass @@ -131,10 +154,12 @@ async def async_setup_platform(hass, config, async_add_entities, config.get(CONF_TILT_INVERT_STATE), config.get(CONF_POSITION_TOPIC), set_position_template, + config.get(CONF_UNIQUE_ID), + discovery_hash )]) -class MqttCover(MqttAvailability, CoverDevice): +class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): """Representation of a cover that can be controlled using MQTT.""" def __init__(self, name, state_topic, command_topic, availability_topic, @@ -143,10 +168,12 @@ class MqttCover(MqttAvailability, CoverDevice): payload_stop, payload_available, payload_not_available, optimistic, value_template, tilt_open_position, tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, - tilt_invert, position_topic, set_position_template): + tilt_invert, position_topic, set_position_template, + unique_id: Optional[str], discovery_hash): """Initialize the cover.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._position = None self._state = None self._name = name @@ -172,10 +199,13 @@ class MqttCover(MqttAvailability, CoverDevice): self._tilt_invert = tilt_invert self._position_topic = position_topic self._set_position_template = set_position_template + self._unique_id = unique_id + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def tilt_updated(topic, payload, qos): @@ -387,3 +417,8 @@ class MqttCover(MqttAvailability, CoverDevice): if self._tilt_invert: position = self._tilt_max - position + offset return position + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 413794505db..78b6f891f11 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.components.cover import ( CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN) from homeassistant.const import ( - CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, - STATE_OPENING) + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_OPEN, + STATE_CLOSING, STATE_OPENING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pymyq==0.0.15'] @@ -23,6 +23,7 @@ DEFAULT_NAME = 'myq' MYQ_TO_HASS = { 'closed': STATE_CLOSED, + 'open': STATE_OPEN, 'closing': STATE_CLOSING, 'opening': STATE_OPENING } diff --git a/homeassistant/components/cover/rflink.py b/homeassistant/components/cover/rflink.py index e50fa488b92..41a4c2af045 100644 --- a/homeassistant/components/cover/rflink.py +++ b/homeassistant/components/cover/rflink.py @@ -92,9 +92,9 @@ class RflinkCover(RflinkCommand, CoverDevice): self.cancel_queued_send_commands() command = event['command'] - if command in ['on', 'allon']: + if command in ['on', 'allon', 'up']: self._state = True - elif command in ['off', 'alloff']: + elif command in ['off', 'alloff', 'down']: self._state = False @property @@ -105,7 +105,12 @@ class RflinkCover(RflinkCommand, CoverDevice): @property def is_closed(self): """Return if the cover is closed.""" - return None + return not self._state + + @property + def assumed_state(self): + """Return True because covers can be stopped midway.""" + return True def async_close_cover(self, **kwargs): """Turn the device close.""" diff --git a/homeassistant/components/cover/tuya.py b/homeassistant/components/cover/tuya.py index 6ab8581602f..a3a3db972e9 100644 --- a/homeassistant/components/cover/tuya.py +++ b/homeassistant/components/cover/tuya.py @@ -45,6 +45,11 @@ class TuyaCover(TuyaDevice, CoverDevice): @property def is_closed(self): """Return if the cover is closed or not.""" + state = self.tuya.state() + if state == 1: + return False + if state == 2: + return True return None def open_cover(self, **kwargs): diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 8c8c88ecb87..258087702e0 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -9,10 +9,9 @@ https://home-assistant.io/components/cover.zwave/ import logging from homeassistant.components.cover import ( DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION) -from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components import zwave -from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import -from homeassistant.components.zwave import workaround +from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import + ZWaveDeviceEntity, async_setup_platform, workaround) from homeassistant.components.cover import CoverDevice _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 0a9e6fdee3f..10eb9f5bc73 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -28,6 +28,6 @@ "title": "Opcions de configuraci\u00f3 addicionals per deCONZ" } }, - "title": "deCONZ" + "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 51b496906a2..645daa56f6b 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -14,10 +14,10 @@ "host": "Host", "port": "Port (Standartwert : '80')" }, - "title": "Definieren Sie den deCONZ-Gateway" + "title": "Definiere das deCONZ-Gateway" }, "link": { - "description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"", + "description": "Entsperre dein deCONZ-Gateway, um dich bei Home Assistant zu registrieren. \n\n 1. Gehe zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" }, "options": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 02f174cd59f..56399a3c6fd 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels" + "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels", + "allow_deconz_groups": "Autoriser l'importation des groupes deCONZ" }, "title": "Options de configuration suppl\u00e9mentaires pour deCONZ" } diff --git a/homeassistant/components/deconz/.translations/he.json b/homeassistant/components/deconz/.translations/he.json index b4b3d54e075..89a2d69950e 100644 --- a/homeassistant/components/deconz/.translations/he.json +++ b/homeassistant/components/deconz/.translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ" + "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ", + "one_instance_only": "\u05d4\u05e8\u05db\u05d9\u05d1 \u05ea\u05d5\u05de\u05da \u05e8\u05e7 \u05d0\u05d7\u05d3 deCONZ \u05dc\u05de\u05e9\u05dc" }, "error": { "no_key": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05e7\u05d1\u05dc \u05de\u05e4\u05ea\u05d7 API" diff --git a/homeassistant/components/deconz/.translations/id.json b/homeassistant/components/deconz/.translations/id.json new file mode 100644 index 00000000000..7d0b3163a40 --- /dev/null +++ b/homeassistant/components/deconz/.translations/id.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge sudah dikonfigurasi", + "no_bridges": "deCONZ bridges tidak ditemukan", + "one_instance_only": "Komponen hanya mendukung satu instance deCONZ" + }, + "error": { + "no_key": "Tidak bisa mendapatkan kunci API" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (nilai default: '80')" + }, + "title": "Tentukan deCONZ gateway" + }, + "link": { + "description": "Buka gerbang deCONZ Anda untuk mendaftar dengan Home Assistant. \n\n 1. Pergi ke pengaturan sistem deCONZ \n 2. Tekan tombol \"Buka Kunci Gateway\"", + "title": "Tautan dengan deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Izinkan mengimpor sensor virtual", + "allow_deconz_groups": "Izinkan mengimpor grup deCONZ" + }, + "title": "Opsi konfigurasi tambahan untuk deCONZ" + } + }, + "title": "deCONZ Zigbee gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index a584a1db9b5..a501951540b 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4 \ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4" + "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4" }, "error": { "no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/deconz/.translations/nn.json b/homeassistant/components/deconz/.translations/nn.json new file mode 100644 index 00000000000..4bdc4b4c1be --- /dev/null +++ b/homeassistant/components/deconz/.translations/nn.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Brua er allereie konfigurert", + "no_bridges": "Oppdaga ingen deCONZ-bruer", + "one_instance_only": "Komponenten st\u00f8ttar berre \u00e9in deCONZ-instans" + }, + "error": { + "no_key": "Kunne ikkje f\u00e5 ein API-n\u00f8kkel" + }, + "step": { + "init": { + "data": { + "host": "Vert", + "port": "Port (standardverdi: '80')" + }, + "title": "Definer deCONZ-gateway" + }, + "link": { + "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere den med Home Assistant.\n\n1. G\u00e5 til systeminnstillingane til deCONZ\n2. Trykk p\u00e5 \"L\u00e5s opp gateway\"-knappen", + "title": "Link med deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Tillat importering av virtuelle sensorar", + "allow_deconz_groups": "Tillat importering av deCONZ-grupper" + }, + "title": "Ekstra konfigurasjonsalternativ for deCONZ" + } + }, + "title": "deCONZ Zigbee gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 55518b7da53..27868814eab 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -28,6 +28,6 @@ "title": "Ekstra konfigurasjonsalternativer for deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index e9f797d95f9..56b03c89a37 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -24,7 +24,10 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==45'] +REQUIREMENTS = ['pydeconz==47'] + +SUPPORTED_PLATFORMS = ['binary_sensor', 'cover', + 'light', 'scene', 'sensor', 'switch'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -46,6 +49,8 @@ SERVICE_SCHEMA = vol.Schema({ vol.Required(SERVICE_DATA): dict, }) +SERVICE_DEVICE_REFRESH = 'device_refresh' + async def async_setup(hass, config): """Load configuration for deCONZ component. @@ -84,15 +89,17 @@ async def async_setup_entry(hass, config_entry): @callback def async_add_device_callback(device_type, device): """Handle event of new device creation in deCONZ.""" + if not isinstance(device, list): + device = [device] async_dispatcher_send( - hass, 'deconz_new_{}'.format(device_type), [device]) + hass, 'deconz_new_{}'.format(device_type), device) session = aiohttp_client.async_get_clientsession(hass) deconz = DeconzSession(hass.loop, session, **config_entry.data, async_add_device=async_add_device_callback) result = await deconz.async_load_parameters() + if result is False: - _LOGGER.error("Failed to communicate with deCONZ") return False hass.data[DOMAIN] = deconz @@ -100,7 +107,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DATA_DECONZ_EVENT] = [] hass.data[DATA_DECONZ_UNSUB] = [] - for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']: + for component in SUPPORTED_PLATFORMS: hass.async_create_task(hass.config_entries.async_forward_entry_setup( config_entry, component)) @@ -123,7 +130,7 @@ async def async_setup_entry(hass, config_entry): device_registry = await \ hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( - config_entry=config_entry.entry_id, + config_entry_id=config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, deconz.config.mac)}, identifiers={(DOMAIN, deconz.config.bridgeid)}, manufacturer='Dresden Elektronik', model=deconz.config.modelid, @@ -149,16 +156,60 @@ async def async_setup_entry(hass, config_entry): data = call.data.get(SERVICE_DATA) deconz = hass.data[DOMAIN] if entity_id: + entities = hass.data.get(DATA_DECONZ_ID) + if entities: field = entities.get(entity_id) + if field is None: _LOGGER.error('Could not find the entity %s', entity_id) return + await deconz.async_put_state(field, data) + hass.services.async_register( DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA) + async def async_refresh_devices(call): + """Refresh available devices from deCONZ.""" + deconz = hass.data[DOMAIN] + + groups = list(deconz.groups.keys()) + lights = list(deconz.lights.keys()) + scenes = list(deconz.scenes.keys()) + sensors = list(deconz.sensors.keys()) + + if not await deconz.async_load_parameters(): + return + + async_add_device_callback( + 'group', [group + for group_id, group in deconz.groups.items() + if group_id not in groups] + ) + + async_add_device_callback( + 'light', [light + for light_id, light in deconz.lights.items() + if light_id not in lights] + ) + + async_add_device_callback( + 'scene', [scene + for scene_id, scene in deconz.scenes.items() + if scene_id not in scenes] + ) + + async_add_device_callback( + 'sensor', [sensor + for sensor_id, sensor in deconz.sensors.items() + if sensor_id not in sensors] + ) + + hass.services.async_register( + DOMAIN, SERVICE_DEVICE_REFRESH, async_refresh_devices) + @callback def deconz_shutdown(event): """ @@ -180,7 +231,7 @@ async def async_unload_entry(hass, config_entry): hass.services.async_remove(DOMAIN, SERVICE_DECONZ) deconz.close() - for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']: + for component in SUPPORTED_PLATFORMS: await hass.config_entries.async_forward_entry_unload( config_entry, component) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index fb2eb54232a..65fcf51b930 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -2,7 +2,7 @@ import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.core import callback from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import aiohttp_client @@ -23,10 +23,11 @@ def configured_hosts(hass): @config_entries.HANDLERS.register(DOMAIN) -class DeconzFlowHandler(data_entry_flow.FlowHandler): +class DeconzFlowHandler(config_entries.ConfigFlow): """Handle a deCONZ config flow.""" VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the deCONZ config flow.""" diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e629d57f201..617d231f92e 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -16,6 +16,8 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' ATTR_DARK = 'dark' ATTR_ON = 'on' +COVER_TYPES = ["Level controllable output"] + POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"] SIRENS = ["Warning device"] SWITCH_TYPES = POWER_PLUGS + SIRENS diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index 78bf7041a93..fa0fb8e14a4 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -1,4 +1,3 @@ - configure: description: Set attribute of device in deCONZ. See https://home-assistant.io/components/deconz/#device-services for details. fields: @@ -11,3 +10,6 @@ configure: data: description: Data is a json object with what data you want to alter. example: '{"on": true}' + +device_refresh: + description: Refresh device lists from deCONZ. \ No newline at end of file diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 641ade7308b..b766513f1f4 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.util.dt as dt_util -from homeassistant.const import STATE_HOME, STATE_NOT_HOME +from homeassistant.const import ( + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, STATE_NOT_HOME) +from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change) from homeassistant.helpers.sun import is_up, get_astral_event_next @@ -86,9 +88,12 @@ def async_setup(hass, config): """Turn on lights.""" if not device_tracker.is_on() or light.is_on(light_id): return - light.async_turn_on(light_id, - transition=LIGHT_TRANSITION_TIME.seconds, - profile=light_profile) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, dict( + entity_id=light_id, + transition=LIGHT_TRANSITION_TIME.seconds, + profile=light_profile))) def async_turn_on_factory(light_id): """Generate turn on callbacks as factory.""" @@ -138,7 +143,10 @@ def async_setup(hass, config): # Do we need lights? if light_needed: logger.info("Home coming event for %s. Turning lights on", entity) - light.async_turn_on(light_ids, profile=light_profile) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, + dict(entity_id=light_ids, profile=light_profile))) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -151,7 +159,10 @@ def async_setup(hass, config): # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.async_turn_on(light_id) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, + dict(entity_id=light_id))) else: # If this light didn't happen to be turned on yet so @@ -173,7 +184,9 @@ def async_setup(hass, config): logger.info( "Everyone has left but there are lights on. Turning them off") - light.async_turn_off(light_ids) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_OFF, dict(entity_id=light_ids))) async_track_state_change( hass, device_group, turn_off_lights_when_all_leave, diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 408672a974f..af1bb1cd9b5 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,6 +15,7 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone +from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, SERVICE_SET from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError @@ -319,9 +320,13 @@ class DeviceTracker: # During init, we ignore the group if self.group and self.track_new: - self.group.async_set_group( - util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, - name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) + self.hass.async_create_task( + self.hass.async_call( + DOMAIN_GROUP, SERVICE_SET, dict( + object_id=util.slugify(GROUP_NAME_ALL_DEVICES), + visible=False, + name=GROUP_NAME_ALL_DEVICES, + add=[device.entity_id]))) self.hass.bus.async_fire(EVENT_NEW_DEVICE, { ATTR_ENTITY_ID: device.entity_id, @@ -354,10 +359,13 @@ class DeviceTracker: entity_ids = [dev.entity_id for dev in self.devices.values() if dev.track] - self.group = self.hass.components.group - self.group.async_set_group( - util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, - name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids) + self.hass.async_create_task( + self.hass.services.async_call( + DOMAIN_GROUP, SERVICE_SET, dict( + object_id=util.slugify(GROUP_NAME_ALL_DEVICES), + visible=False, + name=GROUP_NAME_ALL_DEVICES, + entities=entity_ids))) @callback def async_update_stale(self, now: dt_util.dt.datetime): diff --git a/homeassistant/components/device_tracker/bbox.py b/homeassistant/components/device_tracker/bbox.py index 6d870364dcb..297e98e548a 100644 --- a/homeassistant/components/device_tracker/bbox.py +++ b/homeassistant/components/device_tracker/bbox.py @@ -5,19 +5,30 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.bbox/ """ from collections import namedtuple -import logging from datetime import timedelta +import logging -import homeassistant.util.dt as dt_util -from homeassistant.components.device_tracker import DOMAIN, DeviceScanner +import voluptuous as vol + +from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util REQUIREMENTS = ['pybbox==0.0.5-alpha'] _LOGGER = logging.getLogger(__name__) +DEFAULT_HOST = '192.168.1.254' + MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, +}) + def get_scanner(hass, config): """Validate the configuration and return a Bbox scanner.""" @@ -33,6 +44,9 @@ class BboxDeviceScanner(DeviceScanner): """This class scans for devices connected to the bbox.""" def __init__(self, config): + """Get host from config.""" + self.host = config[CONF_HOST] + """Initialize the scanner.""" self.last_results = [] # type: List[Device] @@ -64,7 +78,7 @@ class BboxDeviceScanner(DeviceScanner): import pybbox - box = pybbox.Bbox() + box = pybbox.Bbox(ip=self.host) result = box.get_all_connected_devices() now = dt_util.now() diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index d9cda24b699..47b86ab9ab2 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -6,35 +6,25 @@ https://home-assistant.io/components/device_tracker.bluetooth_le_tracker/ """ import logging -import voluptuous as vol from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import ( YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, - PLATFORM_SCHEMA, load_config, SOURCE_TYPE_BLUETOOTH_LE + load_config, SOURCE_TYPE_BLUETOOTH_LE ) import homeassistant.util.dt as dt_util -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['gattlib==0.20150805'] +REQUIREMENTS = ['pygatt==3.2.0'] BLE_PREFIX = 'BLE_' MIN_SEEN_NEW = 5 -CONF_SCAN_DURATION = 'scan_duration' -CONF_BLUETOOTH_DEVICE = 'device_id' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SCAN_DURATION, default=10): cv.positive_int, - vol.Optional(CONF_BLUETOOTH_DEVICE, default='hci0'): cv.string -}) def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" # pylint: disable=import-error - from gattlib import DiscoveryService - + import pygatt new_devices = {} def see_device(address, name, new_device=False): @@ -61,17 +51,17 @@ def setup_scanner(hass, config, see, discovery_info=None): """Discover Bluetooth LE devices.""" _LOGGER.debug("Discovering Bluetooth LE devices") try: - service = DiscoveryService(ble_dev_id) - devices = service.discover(duration) + adapter = pygatt.GATTToolBackend() + devs = adapter.scan() + + devices = {x['address']: x['name'] for x in devs} _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) except RuntimeError as error: _LOGGER.error("Error during Bluetooth LE scan: %s", error) - devices = [] + return {} return devices yaml_path = hass.config.path(YAML_DEVICES) - duration = config.get(CONF_SCAN_DURATION) - ble_dev_id = config.get(CONF_BLUETOOTH_DEVICE) devs_to_track = [] devs_donot_track = [] @@ -102,11 +92,11 @@ def setup_scanner(hass, config, see, discovery_info=None): """Lookup Bluetooth LE devices and update status.""" devs = discover_ble_devices() for mac in devs_to_track: - _LOGGER.debug("Checking %s", mac) - result = mac in devs - if not result: - # Could not lookup device name + if mac not in devs: continue + + if devs[mac] is None: + devs[mac] = mac see_device(mac, devs[mac]) if track_new: @@ -119,5 +109,4 @@ def setup_scanner(hass, config, see, discovery_info=None): track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval) update_ble(dt_util.utcnow()) - return True diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 217df0aacd4..d22a1ba7c1f 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -80,7 +80,7 @@ def setup_scanner(hass, config, see, discovery_info=None): request_rssi = config.get(CONF_REQUEST_RSSI, False) - def update_bluetooth(): + def update_bluetooth(_): """Update Bluetooth and set timer for the next update.""" update_bluetooth_once() track_point_in_utc_time( @@ -111,7 +111,7 @@ def setup_scanner(hass, config, see, discovery_info=None): """Update bluetooth devices on demand.""" update_bluetooth_once() - update_bluetooth() + update_bluetooth(dt_util.utcnow()) hass.services.register( DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 8c21e71bd30..c0dcd2f00a7 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -11,13 +11,15 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, SOURCE_TYPE_GPS) -from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + ATTR_ID, CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_CHARGING, + ATTR_BATTERY_LEVEL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify +from homeassistant.util import slugify, dt as dt_util -REQUIREMENTS = ['locationsharinglib==2.0.11'] +REQUIREMENTS = ['locationsharinglib==3.0.2'] _LOGGER = logging.getLogger(__name__) @@ -92,8 +94,10 @@ class GoogleMapsScanner: ATTR_ADDRESS: person.address, ATTR_FULL_NAME: person.full_name, ATTR_ID: person.id, - ATTR_LAST_SEEN: person.datetime, + ATTR_LAST_SEEN: dt_util.as_utc(person.datetime), ATTR_NICKNAME: person.nickname, + ATTR_BATTERY_CHARGING: person.charging, + ATTR_BATTERY_LEVEL: person.battery_level } self.see( dev_id=dev_id, diff --git a/homeassistant/components/device_tracker/huawei_lte.py b/homeassistant/components/device_tracker/huawei_lte.py new file mode 100644 index 00000000000..4b4eb3f001a --- /dev/null +++ b/homeassistant/components/device_tracker/huawei_lte.py @@ -0,0 +1,65 @@ +""" +Support for Huawei LTE routers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.huawei_lte/ +""" +from typing import Any, Dict, List, Optional + +import attr +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA, DeviceScanner, +) +from homeassistant.const import CONF_URL +from ..huawei_lte import DATA_KEY, RouterData + + +DEPENDENCIES = ['huawei_lte'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_URL): cv.url, +}) + + +def get_scanner(hass, config): + """Get a Huawei LTE router scanner.""" + data = hass.data[DATA_KEY].get_data(config) + return HuaweiLteScanner(data) + + +@attr.s +class HuaweiLteScanner(DeviceScanner): + """Huawei LTE router scanner.""" + + data = attr.ib(type=RouterData) + + _hosts = attr.ib(init=False, factory=dict) + + def scan_devices(self) -> List[str]: + """Scan for devices.""" + self.data.update() + self._hosts = { + x["MacAddress"]: x + for x in self.data["wlan_host_list.Hosts.Host"] + if x.get("MacAddress") + } + return list(self._hosts) + + def get_device_name(self, device: str) -> Optional[str]: + """Get name for a device.""" + host = self._hosts.get(device) + return host.get("HostName") or None if host else None + + def get_extra_attributes(self, device: str) -> Dict[str, Any]: + """ + Get extra attributes of a device. + + Some known extra attributes that may be returned in the dict + include MacAddress (MAC address), ID (client ID), IpAddress + (IP address), AssociatedSsid (associated SSID), AssociatedTime + (associated time in seconds), and HostName (host name). + """ + return self._hosts.get(device) or {} diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index dfc66a412c3..320468159e0 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) -REQUIREMENTS = ['librouteros==2.1.0'] +REQUIREMENTS = ['librouteros==2.1.1'] MTK_DEFAULT_API_PORT = '8728' diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 75929e76462..f1b5bb67428 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==2.0.0'] +REQUIREMENTS = ['netdisco==2.1.0'] DOMAIN = 'discovery' @@ -48,6 +48,7 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_DECONZ: 'deconz', 'google_cast': 'cast', SERVICE_HUE: 'hue', + SERVICE_IKEA_TRADFRI: 'tradfri', 'sonos': 'sonos', 'igd': 'upnp', } @@ -56,7 +57,6 @@ SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_NETGEAR: ('device_tracker', None), SERVICE_WEMO: ('wemo', None), - SERVICE_IKEA_TRADFRI: ('tradfri', None), SERVICE_HASSIO: ('hassio', None), SERVICE_AXIS: ('axis', None), SERVICE_APPLE_TV: ('apple_tv', None), diff --git a/homeassistant/components/duckdns.py b/homeassistant/components/duckdns.py index 178e1579538..6ec2e6b1e03 100644 --- a/homeassistant/components/duckdns.py +++ b/homeassistant/components/duckdns.py @@ -11,7 +11,6 @@ import logging import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -40,15 +39,6 @@ SERVICE_TXT_SCHEMA = vol.Schema({ }) -@bind_hass -@asyncio.coroutine -def async_set_txt(hass, txt): - """Set the txt record. Pass in None to remove it.""" - yield from hass.services.async_call(DOMAIN, SERVICE_SET_TXT, { - ATTR_TXT: txt - }, blocking=True) - - @asyncio.coroutine def async_setup(hass, config): """Initialize the DuckDNS component.""" diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index 2e51b048d15..8cbe95ee685 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \ EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['sucks==0.9.1'] +REQUIREMENTS = ['sucks==0.9.3'] _LOGGER = logging.getLogger(__name__) @@ -59,8 +59,9 @@ def setup(hass, config): _LOGGER.debug("Ecobot devices: %s", devices) for device in devices: - _LOGGER.info("Discovered Ecovacs device on account: %s", - device['nick']) + _LOGGER.info( + "Discovered Ecovacs device on account: %s with nickname %s", + device['did'], device['nick']) vacbot = VacBot(ecovacs_api.uid, ecovacs_api.REALM, ecovacs_api.resource, @@ -74,7 +75,7 @@ def setup(hass, config): """Shut down open connections to Ecovacs XMPP server.""" for device in hass.data[ECOVACS_DEVICES]: _LOGGER.info("Shutting down connection to Ecovacs device %s", - device.vacuum['nick']) + device.vacuum['did']) device.disconnect() # Listen for HA stop to disconnect. diff --git a/homeassistant/components/edp_redy.py b/homeassistant/components/edp_redy.py new file mode 100644 index 00000000000..caf4ad41d99 --- /dev/null +++ b/homeassistant/components/edp_redy.py @@ -0,0 +1,135 @@ +""" +Support for EDP re:dy. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/edp_redy/ +""" + +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, + EVENT_HOMEASSISTANT_START) +from homeassistant.core import callback +from homeassistant.helpers import discovery, dispatcher, aiohttp_client +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_time +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'edp_redy' +EDP_REDY = 'edp_redy' +DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +UPDATE_INTERVAL = 30 + +REQUIREMENTS = ['edp_redy==0.0.2'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the EDP re:dy component.""" + from edp_redy import EdpRedySession + + session = EdpRedySession(config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + aiohttp_client.async_get_clientsession(hass), + hass.loop) + hass.data[EDP_REDY] = session + platform_loaded = False + + async def async_update_and_sched(time): + update_success = await session.async_update() + + if update_success: + nonlocal platform_loaded + # pylint: disable=used-before-assignment + if not platform_loaded: + for component in ['sensor', 'switch']: + await discovery.async_load_platform(hass, component, + DOMAIN, {}, config) + platform_loaded = True + + dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC) + + # schedule next update + async_track_point_in_time(hass, async_update_and_sched, + time + timedelta(seconds=UPDATE_INTERVAL)) + + async def start_component(event): + _LOGGER.debug("Starting updates") + await async_update_and_sched(dt_util.utcnow()) + + # only start fetching data after HA boots to prevent delaying the boot + # process + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component) + + return True + + +class EdpRedyDevice(Entity): + """Representation a base re:dy device.""" + + def __init__(self, session, device_id, name): + """Initialize the device.""" + self._session = session + self._state = None + self._is_available = True + self._device_state_attributes = {} + self._id = device_id + self._unique_id = device_id + self._name = name if name else device_id + + async def async_added_to_hass(self): + """Subscribe to the data updates topic.""" + dispatcher.async_dispatcher_connect( + self.hass, DATA_UPDATE_TOPIC, self._data_updated) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def available(self): + """Return True if entity is available.""" + return self._is_available + + @property + def should_poll(self): + """Return the polling state. No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device_state_attributes + + @callback + def _data_updated(self): + """Update state, trigger updates.""" + self.async_schedule_update_ha_state(True) + + def _parse_data(self, data): + """Parse data received from the server.""" + if "OutOfOrder" in data: + try: + self._is_available = not data['OutOfOrder'] + except ValueError: + _LOGGER.error( + "Could not parse OutOfOrder for %s", self._id) + self._is_available = False diff --git a/homeassistant/components/evohome.py b/homeassistant/components/evohome.py new file mode 100644 index 00000000000..ceeec407b05 --- /dev/null +++ b/homeassistant/components/evohome.py @@ -0,0 +1,145 @@ +"""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 component, please refer to the documentation at +https://home-assistant.io/components/evohome/ +""" + +# Glossary: +# TCS - temperature control system (a.k.a. Controller, Parent), which can +# have up to 13 Children: +# 0-12 Heating zones (a.k.a. Zone), and +# 0-1 DHW controller, (a.k.a. Boiler) + +import logging + +from requests.exceptions import HTTPError +import voluptuous as vol + +from homeassistant.const import ( + CONF_USERNAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + HTTP_BAD_REQUEST +) + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['evohomeclient==0.2.7'] +# If ever > 0.2.7, re-check the work-around wrapper is still required when +# instantiating the client, below. + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'evohome' +DATA_EVOHOME = 'data_' + DOMAIN + +CONF_LOCATION_IDX = 'location_idx' +MAX_TEMP = 28 +MIN_TEMP = 5 +SCAN_INTERVAL_DEFAULT = 180 +SCAN_INTERVAL_MAX = 300 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int, + }), +}, extra=vol.ALLOW_EXTRA) + +# These are used to help prevent E501 (line too long) violations. +GWS = 'gateways' +TCS = 'temperatureControlSystems' + + +def setup(hass, config): + """Create a Honeywell (EMEA/EU) evohome CH/DHW system. + + One controller with 0+ heating zones (e.g. TRVs, relays) and, optionally, a + DHW controller. Does not work for US-based systems. + """ + evo_data = hass.data[DATA_EVOHOME] = {} + evo_data['timers'] = {} + + evo_data['params'] = dict(config[DOMAIN]) + evo_data['params'][CONF_SCAN_INTERVAL] = SCAN_INTERVAL_DEFAULT + + from evohomeclient2 import EvohomeClient + + _LOGGER.debug("setup(): API call [4 request(s)]: client.__init__()...") + + try: + # There's a bug in evohomeclient2 v0.2.7: the client.__init__() sets + # the root loglevel when EvohomeClient(debug=?), so remember it now... + log_level = logging.getLogger().getEffectiveLevel() + + client = EvohomeClient( + evo_data['params'][CONF_USERNAME], + evo_data['params'][CONF_PASSWORD], + debug=False + ) + # ...then restore it to what it was before instantiating the client + logging.getLogger().setLevel(log_level) + + except HTTPError as err: + if err.response.status_code == HTTP_BAD_REQUEST: + _LOGGER.error( + "Failed to establish a connection with evohome web servers, " + "Check your username (%s), and password are correct." + "Unable to continue. Resolve any errors and restart HA.", + evo_data['params'][CONF_USERNAME] + ) + return False # unable to continue + + raise # we dont handle any other HTTPErrors + + finally: # Redact username, password as no longer needed. + evo_data['params'][CONF_USERNAME] = 'REDACTED' + evo_data['params'][CONF_PASSWORD] = 'REDACTED' + + evo_data['client'] = client + + # Redact any installation data we'll never need. + if client.installation_info[0]['locationInfo']['locationId'] != 'REDACTED': + for loc in client.installation_info: + loc['locationInfo']['streetAddress'] = 'REDACTED' + loc['locationInfo']['city'] = 'REDACTED' + loc['locationInfo']['locationOwner'] = 'REDACTED' + loc[GWS][0]['gatewayInfo'] = 'REDACTED' + + # Pull down the installation configuration. + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + try: + evo_data['config'] = client.installation_info[loc_idx] + + except IndexError: + _LOGGER.warning( + "setup(): Parameter '%s' = %s , is outside its range (0-%s)", + CONF_LOCATION_IDX, + loc_idx, + len(client.installation_info) - 1 + ) + + return False # unable to continue + + evo_data['status'] = {} + + if _LOGGER.isEnabledFor(logging.DEBUG): + tmp_loc = dict(evo_data['config']) + tmp_loc['locationInfo']['postcode'] = 'REDACTED' + tmp_tcs = tmp_loc[GWS][0][TCS][0] + if 'zones' in tmp_tcs: + tmp_tcs['zones'] = '...' + if 'dhw' in tmp_tcs: + tmp_tcs['dhw'] = '...' + + _LOGGER.debug("setup(), location = %s", tmp_loc) + + load_platform(hass, 'climate', DOMAIN) + + return True diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index f2704e84bc5..36b075747e0 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -4,7 +4,6 @@ Provides functionality to interact with fans. For more details about this component, please refer to the documentation at https://home-assistant.io/components/fan/ """ -import asyncio from datetime import timedelta import functools as ft import logging @@ -98,84 +97,12 @@ def is_on(hass, entity_id: str = None) -> bool: return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] -@bind_hass -def turn_on(hass, entity_id: str = None, speed: str = None) -> None: - """Turn all or specified fan on.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_SPEED, speed), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id: str = None) -> None: - """Turn all or specified fan off.""" - 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: str = None) -> None: - """Toggle all or specified fans.""" - data = { - ATTR_ENTITY_ID: entity_id - } - - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def oscillate(hass, entity_id: str = None, - should_oscillate: bool = True) -> None: - """Set oscillation on all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_OSCILLATING, should_oscillate), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_OSCILLATE, data) - - -@bind_hass -def set_speed(hass, entity_id: str = None, speed: str = None) -> None: - """Set speed for all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_SPEED, speed), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_SET_SPEED, data) - - -@bind_hass -def set_direction(hass, entity_id: str = None, direction: str = None) -> None: - """Set direction for all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_DIRECTION, direction), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) - - -@asyncio.coroutine -def async_setup(hass, config: dict): +async def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_TURN_ON, FAN_TURN_ON_SCHEMA, @@ -205,6 +132,16 @@ def async_setup(hass, config: dict): return True +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class FanEntity(ToggleEntity): """Representation of a fan.""" diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index db3cfab3608..3e1ad2704e7 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.mqtt/ """ import logging +from typing import Optional import voluptuous as vol @@ -14,9 +15,9 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, @@ -41,6 +42,7 @@ CONF_PAYLOAD_LOW_SPEED = 'payload_low_speed' CONF_PAYLOAD_MEDIUM_SPEED = 'payload_medium_speed' CONF_PAYLOAD_HIGH_SPEED = 'payload_high_speed' CONF_SPEED_LIST = 'speeds' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Fan' DEFAULT_PAYLOAD_ON = 'ON' @@ -74,6 +76,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -83,6 +86,10 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttFan( config.get(CONF_NAME), { @@ -116,18 +123,22 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_UNIQUE_ID), + discovery_hash, )]) -class MqttFan(MqttAvailability, FanEntity): +class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity): """A MQTT fan component.""" def __init__(self, name, topic, templates, qos, retain, payload, speed_list, optimistic, availability_topic, payload_available, - payload_not_available): + payload_not_available, unique_id: Optional[str], + discovery_hash): """Initialize the MQTT fan.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._topic = topic self._qos = qos @@ -148,10 +159,13 @@ class MqttFan(MqttAvailability, FanEntity): is not None and SUPPORT_OSCILLATE) self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) + self._unique_id = unique_id + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) templates = {} for key, tpl in list(self._templates.items()): @@ -315,3 +329,8 @@ class MqttFan(MqttAvailability, FanEntity): if self._optimistic_oscillation: self._oscillation = oscillating self.async_schedule_update_ha_state() + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py index 2612c065393..b5615f18d73 100644 --- a/homeassistant/components/fan/zha.py +++ b/homeassistant/components/fan/zha.py @@ -4,7 +4,6 @@ Fans on Zigbee Home Automation networks. For more details on this platform, please refer to the documentation at https://home-assistant.io/components/fan.zha/ """ -import asyncio import logging from homeassistant.components import zha from homeassistant.components.fan import ( @@ -38,9 +37,8 @@ VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)} SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Zigbee Home Automation fans.""" discovery_info = zha.get_discovery_info(hass, discovery_info) if discovery_info is None: @@ -76,32 +74,36 @@ class ZhaFan(zha.Entity, FanEntity): return False return self._state != SPEED_OFF - @asyncio.coroutine - def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn the entity on.""" if speed is None: speed = SPEED_MEDIUM - yield from self.async_set_speed(speed) + await self.async_set_speed(speed) - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" - yield from self.async_set_speed(SPEED_OFF) + await self.async_set_speed(SPEED_OFF) - @asyncio.coroutine - def async_set_speed(self, speed: str) -> None: + async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - yield from self._endpoint.fan.write_attributes({ - 'fan_mode': SPEED_TO_VALUE[speed]}) + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.fan.write_attributes( + {'fan_mode': SPEED_TO_VALUE[speed]} + ) + except DeliveryError as ex: + _LOGGER.error("%s: Could not set speed: %s", self.entity_id, ex) + return self._state = speed self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" - result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode']) + result = await zha.safe_read(self._endpoint.fan, ['fan_mode'], + allow_cache=False, + only_cache=(not self._initialized)) new_value = result.get('fan_mode', None) self._state = VALUE_TO_SPEED.get(new_value, None) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5508aa76acf..023e75aac85 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,16 +21,14 @@ from homeassistant.components import websocket_api from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180831.0'] +REQUIREMENTS = ['home-assistant-frontend==20180927.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', - 'auth', 'onboarding'] + 'auth', 'onboarding', 'lovelace'] CONF_THEMES = 'themes' CONF_EXTRA_HTML_URL = 'extra_html_url' @@ -108,10 +106,6 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, vol.Required('language'): str, }) -WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' -SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_LOVELACE_UI, -}) class Panel: @@ -208,9 +202,6 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, - SCHEMA_GET_LOVELACE_UI) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -353,11 +344,12 @@ class AuthorizeView(HomeAssistantView): _is_latest(self.js_option, request) if latest: - location = '/frontend_latest/authorize.html' + base = 'frontend_latest' else: - location = '/frontend_es5/authorize.html' + base = 'frontend_es5' - location += '?{}'.format(request.query_string) + location = "/{}/authorize.html{}".format( + base, str(request.url.relative())[15:]) return web.Response(status=302, headers={ 'location': location @@ -530,28 +522,3 @@ def websocket_get_translations(hass, connection, msg): )) hass.async_add_job(send_translations()) - - -def websocket_lovelace_config(hass, connection, msg): - """Send lovelace UI config over websocket config.""" - async def send_exp_config(): - """Send lovelace frontend config.""" - error = None - try: - config = await hass.async_add_job( - load_yaml, hass.config.path('ui-lovelace.yaml')) - message = websocket_api.result_message( - msg['id'], config - ) - except FileNotFoundError: - error = ('file_not_found', - 'Could not find ui-lovelace.yaml in your config dir.') - except HomeAssistantError as err: - error = 'load_error', str(err) - - if error is not None: - message = websocket_api.error_message(msg['id'], *error) - - connection.send_message_outside(message) - - hass.async_add_job(send_exp_config()) diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 67ed9520fa4..66753aad221 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -14,6 +14,7 @@ from typing import Optional from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py index 8e8d8211086..ddec369e696 100644 --- a/homeassistant/components/geo_location/demo.py +++ b/homeassistant/components/geo_location/demo.py @@ -8,7 +8,6 @@ import logging import random from datetime import timedelta from math import pi, cos, sin, radians - from typing import Optional from homeassistant.components.geo_location import GeoLocationEvent diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py new file mode 100644 index 00000000000..bb17fb2450e --- /dev/null +++ b/homeassistant/components/geo_location/geo_json_events.py @@ -0,0 +1,196 @@ +""" +Generic GeoJSON events platform. + +Retrieves current events (typically incidents or alerts) in GeoJSON format, and +displays information on events filtered by distance to the HA instance's +location. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/geo_location/geo_json_events/ +""" +import logging +from datetime import timedelta +from typing import Optional + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.geo_location import GeoLocationEvent +from homeassistant.const import CONF_RADIUS, CONF_URL, CONF_SCAN_INTERVAL, \ + EVENT_HOMEASSISTANT_START +from homeassistant.components.geo_location import PLATFORM_SCHEMA +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['geojson_client==0.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_EXTERNAL_ID = 'external_id' + +DEFAULT_RADIUS_IN_KM = 20.0 +DEFAULT_UNIT_OF_MEASUREMENT = "km" + +SCAN_INTERVAL = timedelta(minutes=5) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): + vol.Coerce(float), +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the GeoJSON Events platform.""" + url = config[CONF_URL] + scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + radius_in_km = config[CONF_RADIUS] + # Initialize the entity manager. + GeoJsonFeedManager(hass, add_entities, scan_interval, url, radius_in_km) + + +class GeoJsonFeedManager: + """Feed Manager for GeoJSON feeds.""" + + def __init__(self, hass, add_entities, scan_interval, url, radius_in_km): + """Initialize the GeoJSON Feed Manager.""" + from geojson_client.generic_feed import GenericFeed + self._hass = hass + self._feed = GenericFeed((hass.config.latitude, hass.config.longitude), + filter_radius=radius_in_km, url=url) + self._add_entities = add_entities + self._scan_interval = scan_interval + self._feed_entries = [] + self._managed_entities = [] + hass.bus.listen_once( + EVENT_HOMEASSISTANT_START, lambda _: self._update()) + self._init_regular_updates() + + def _init_regular_updates(self): + """Schedule regular updates at the specified interval.""" + track_time_interval(self._hass, lambda now: self._update(), + self._scan_interval) + + def _update(self): + """Update the feed and then update connected entities.""" + import geojson_client + status, feed_entries = self._feed.update() + if status == geojson_client.UPDATE_OK: + _LOGGER.debug("Data retrieved %s", feed_entries) + # Keep a copy of all feed entries for future lookups by entities. + self._feed_entries = feed_entries.copy() + keep_entries = self._update_or_remove_entities(feed_entries) + self._generate_new_entities(keep_entries) + elif status == geojson_client.UPDATE_OK_NO_DATA: + _LOGGER.debug("Update successful, but no data received from %s", + self._feed) + else: + _LOGGER.warning("Update not successful, no data received from %s", + self._feed) + # Remove all entities. + self._update_or_remove_entities([]) + + def _update_or_remove_entities(self, feed_entries): + """Update existing entries and remove obsolete entities.""" + _LOGGER.debug("Entries for updating: %s", feed_entries) + remove_entry = None + # Remove obsolete entities for events that have disappeared + managed_entities = self._managed_entities.copy() + for entity in managed_entities: + # Remove entry from previous iteration - if applicable. + if remove_entry: + feed_entries.remove(remove_entry) + remove_entry = None + for entry in feed_entries: + if entity.external_id == entry.external_id: + # Existing entity - update details. + _LOGGER.debug("Existing entity found %s", entity) + remove_entry = entry + entity.schedule_update_ha_state(True) + break + else: + # Remove obsolete entity. + _LOGGER.debug("Entity not current anymore %s", entity) + self._managed_entities.remove(entity) + self._hass.add_job(entity.async_remove()) + # Remove entry from very last iteration - if applicable. + if remove_entry: + feed_entries.remove(remove_entry) + # Return the remaining entries that new entities must be created for. + return feed_entries + + def _generate_new_entities(self, entries): + """Generate new entities for events.""" + new_entities = [] + for entry in entries: + new_entity = GeoJsonLocationEvent(self, entry) + _LOGGER.debug("New entity added %s", new_entity) + new_entities.append(new_entity) + # Add new entities to HA and keep track of them in this manager. + self._add_entities(new_entities, True) + self._managed_entities.extend(new_entities) + + def get_feed_entry(self, external_id): + """Return a feed entry identified by external id.""" + return next((entry for entry in self._feed_entries + if entry.external_id == external_id), None) + + +class GeoJsonLocationEvent(GeoLocationEvent): + """This represents an external event with GeoJSON data.""" + + def __init__(self, feed_manager, feed_entry): + """Initialize entity with data from feed entry.""" + self._feed_manager = feed_manager + self._update_from_feed(feed_entry) + + @property + def should_poll(self): + """No polling needed for GeoJSON location events.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + feed_entry = self._feed_manager.get_feed_entry(self.external_id) + if feed_entry: + self._update_from_feed(feed_entry) + + def _update_from_feed(self, feed_entry): + """Update the internal state from the provided feed entry.""" + self._name = feed_entry.title + self._distance = feed_entry.distance_to_home + self._latitude = feed_entry.coordinates[0] + self._longitude = feed_entry.coordinates[1] + self.external_id = feed_entry.external_id + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return self._name + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return self._distance + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return self._latitude + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return self._longitude + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return DEFAULT_UNIT_OF_MEASUREMENT + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + if self.external_id: + attributes[ATTR_EXTERNAL_ID] = self.external_id + return attributes diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 567a6d84233..8d4ac9f01c9 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/google_assistant/ """ import asyncio import logging +from typing import Dict, Any import aiohttp import async_timeout @@ -13,22 +14,18 @@ import async_timeout import voluptuous as vol # Typing imports -from homeassistant.core import HomeAssistant -from typing import Dict, Any +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.loader import bind_hass from .const import ( - DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, - CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - DEFAULT_EXPOSED_DOMAINS, CONF_AGENT_USER_ID, CONF_API_KEY, + DOMAIN, CONF_PROJECT_ID, CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY, SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT ) -from .auth import GoogleAssistantAuthView from .http import async_register_http _LOGGER = logging.getLogger(__name__) @@ -44,40 +41,28 @@ ENTITY_SCHEMA = vol.Schema({ vol.Optional(CONF_ROOM_HINT): cv.string }) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: { - vol.Required(CONF_PROJECT_ID): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_EXPOSE_BY_DEFAULT, - default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, - vol.Optional(CONF_EXPOSED_DOMAINS, - default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, - vol.Optional(CONF_AGENT_USER_ID, - default=DEFAULT_AGENT_USER_ID): cv.string, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA} - } - }, - extra=vol.ALLOW_EXTRA) +GOOGLE_ASSISTANT_SCHEMA = vol.Schema({ + vol.Required(CONF_PROJECT_ID): cv.string, + vol.Optional(CONF_EXPOSE_BY_DEFAULT, + default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, + vol.Optional(CONF_EXPOSED_DOMAINS, + default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA} +}, extra=vol.PREVENT_EXTRA) - -@bind_hass -def request_sync(hass): - """Request sync.""" - hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: GOOGLE_ASSISTANT_SCHEMA +}, extra=vol.ALLOW_EXTRA) async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) - agent_user_id = config.get(CONF_AGENT_USER_ID) api_key = config.get(CONF_API_KEY) - hass.http.register_view(GoogleAssistantAuthView(hass, config)) async_register_http(hass, config) - async def request_sync_service_handler(call): + async def request_sync_service_handler(call: ServiceCall): """Handle request sync service calls.""" websession = async_get_clientsession(hass) try: @@ -85,7 +70,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): res = await websession.post( REQUEST_SYNC_BASE_URL, params={'key': api_key}, - json={'agent_user_id': agent_user_id}) + json={'agent_user_id': call.context.user_id}) _LOGGER.info("Submitted request_sync request to Google") res.raise_for_status() except aiohttp.ClientResponseError: diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py deleted file mode 100644 index e80b2282066..00000000000 --- a/homeassistant/components/google_assistant/auth.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Google Assistant OAuth View.""" - -import logging - -# Typing imports -# if False: -from aiohttp.web import Request, Response -from typing import Dict, Any - -from homeassistant.core import HomeAssistant -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ( - HTTP_BAD_REQUEST, - HTTP_UNAUTHORIZED, - HTTP_MOVED_PERMANENTLY, -) - -from .const import ( - GOOGLE_ASSISTANT_API_ENDPOINT, - CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN -) - -BASE_OAUTH_URL = 'https://oauth-redirect.googleusercontent.com' -REDIRECT_TEMPLATE_URL = \ - '{}/r/{}#access_token={}&token_type=bearer&state={}' - -_LOGGER = logging.getLogger(__name__) - - -class GoogleAssistantAuthView(HomeAssistantView): - """Handle Google Actions auth requests.""" - - url = GOOGLE_ASSISTANT_API_ENDPOINT + '/auth' - name = 'api:google_assistant:auth' - requires_auth = False - - def __init__(self, hass: HomeAssistant, cfg: Dict[str, Any]) -> None: - """Initialize instance of the view.""" - super().__init__() - - self.project_id = cfg.get(CONF_PROJECT_ID) - self.client_id = cfg.get(CONF_CLIENT_ID) - self.access_token = cfg.get(CONF_ACCESS_TOKEN) - - async def get(self, request: Request) -> Response: - """Handle oauth token request.""" - query = request.query - redirect_uri = query.get('redirect_uri') - if not redirect_uri: - msg = 'missing redirect_uri field' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - if self.project_id not in redirect_uri: - msg = 'missing project_id in redirect_uri' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - state = query.get('state') - if not state: - msg = 'oauth request missing state' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - client_id = query.get('client_id') - if self.client_id != client_id: - msg = 'invalid client id' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_UNAUTHORIZED) - - generated_url = redirect_url(self.project_id, self.access_token, state) - - _LOGGER.info('user login in from Google Assistant') - return self.json_message( - 'redirect success', - status_code=HTTP_MOVED_PERMANENTLY, - headers={'Location': generated_url}) - - -def redirect_url(project_id: str, access_token: str, state: str) -> str: - """Generate the redirect format for the oauth request.""" - return REDIRECT_TEMPLATE_URL.format(BASE_OAUTH_URL, project_id, - access_token, state) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 12888ea2cf6..485b98e8e22 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -8,10 +8,7 @@ CONF_ENTITY_CONFIG = 'entity_config' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_PROJECT_ID = 'project_id' -CONF_ACCESS_TOKEN = 'access_token' -CONF_CLIENT_ID = 'client_id' CONF_ALIASES = 'aliases' -CONF_AGENT_USER_ID = 'agent_user_id' CONF_API_KEY = 'api_key' CONF_ROOM_HINT = 'room' diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 05bc3cbd01c..65af7b932b0 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/google_assistant/ """ import logging -from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response # Typing imports @@ -15,10 +14,8 @@ from homeassistant.core import callback from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, - CONF_ACCESS_TOKEN, CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - CONF_AGENT_USER_ID, CONF_ENTITY_CONFIG, CONF_EXPOSE, ) @@ -31,10 +28,8 @@ _LOGGER = logging.getLogger(__name__) @callback def async_register_http(hass, cfg): """Register HTTP views for Google Assistant.""" - access_token = cfg.get(CONF_ACCESS_TOKEN) expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT) exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) - agent_user_id = cfg.get(CONF_AGENT_USER_ID) entity_config = cfg.get(CONF_ENTITY_CONFIG) or {} def is_exposed(entity) -> bool: @@ -57,9 +52,8 @@ def async_register_http(hass, cfg): return is_default_exposed or explicit_expose - gass_config = Config(is_exposed, agent_user_id, entity_config) hass.http.register_view( - GoogleAssistantView(access_token, gass_config)) + GoogleAssistantView(is_exposed, entity_config)) class GoogleAssistantView(HomeAssistantView): @@ -67,20 +61,19 @@ class GoogleAssistantView(HomeAssistantView): url = GOOGLE_ASSISTANT_API_ENDPOINT name = 'api:google_assistant' - requires_auth = False # Uses access token from oauth flow + requires_auth = True - def __init__(self, access_token, gass_config): + def __init__(self, is_exposed, entity_config): """Initialize the Google Assistant request handler.""" - self.access_token = access_token - self.gass_config = gass_config + self.is_exposed = is_exposed + self.entity_config = entity_config async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" - auth = request.headers.get(AUTHORIZATION, None) - if 'Bearer {}'.format(self.access_token) != auth: - return self.json_message("missing authorization", status_code=401) - message = await request.json() # type: dict + config = Config(self.is_exposed, + request['hass_user'].id, + self.entity_config) result = await async_handle_message( - request.app['hass'], self.gass_config, message) + request.app['hass'], config, message) return self.json(result) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 675e86f9d39..1cb4bf4cb32 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -324,3 +324,11 @@ async def handle_devices_execute(hass, config, payload): }) return {'commands': final_results} + + +def turned_off_response(message): + """Return a device turned off response.""" + return { + 'requestId': message.get('requestId'), + 'payload': {'errorCode': 'deviceTurnedOff'} + } diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 26e80e6f03b..1ee9d4e2364 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1,4 +1,6 @@ """Implement the Smart Home traits.""" +import logging + from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.components import ( climate, @@ -25,6 +27,8 @@ from homeassistant.util import color as color_util, temperature as temp_util from .const import ERR_VALUE_OUT_OF_RANGE from .helpers import SmartHomeError +_LOGGER = logging.getLogger(__name__) + PREFIX_TRAITS = 'action.devices.traits.' TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff' TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness' @@ -317,7 +321,11 @@ class ColorTemperatureTrait(_Trait): response = {} temp = self.state.attributes.get(light.ATTR_COLOR_TEMP) - if temp is not None: + # Some faulty integrations might put 0 in here, raising exception. + if temp == 0: + _LOGGER.warning('Entity %s has incorrect color temperature %s', + self.state.entity_id, temp) + elif temp is not None: response['color'] = { 'temperature': color_util.color_temperature_mired_to_kelvin(temp) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index eda65d1895d..39fd7567c98 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -120,70 +120,6 @@ def is_on(hass, entity_id): return False -@bind_hass -def reload(hass): - """Reload the automation from config.""" - hass.add_job(async_reload, hass) - - -@callback -@bind_hass -def async_reload(hass): - """Reload the automation from config.""" - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD)) - - -@bind_hass -def set_visibility(hass, entity_id=None, visible=True): - """Hide or shows a group.""" - data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible} - hass.services.call(DOMAIN, SERVICE_SET_VISIBILITY, data) - - -@bind_hass -def set_group(hass, object_id, name=None, entity_ids=None, visible=None, - icon=None, view=None, control=None, add=None): - """Create/Update a group.""" - hass.add_job( - async_set_group, hass, object_id, name, entity_ids, visible, icon, - view, control, add) - - -@callback -@bind_hass -def async_set_group(hass, object_id, name=None, entity_ids=None, visible=None, - icon=None, view=None, control=None, add=None): - """Create/Update a group.""" - data = { - key: value for key, value in [ - (ATTR_OBJECT_ID, object_id), - (ATTR_NAME, name), - (ATTR_ENTITIES, entity_ids), - (ATTR_VISIBLE, visible), - (ATTR_ICON, icon), - (ATTR_VIEW, view), - (ATTR_CONTROL, control), - (ATTR_ADD_ENTITIES, add), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SET, data)) - - -@bind_hass -def remove(hass, name): - """Remove a user group.""" - hass.add_job(async_remove, hass, name) - - -@callback -@bind_hass -def async_remove(hass, object_id): - """Remove a user group.""" - data = {ATTR_OBJECT_ID: object_id} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data)) - - @bind_hass def expand_entity_ids(hass, entity_ids): """Return entity_ids with group entity ids replaced by their members. diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json index c92d478c454..00a7d5fd80d 100644 --- a/homeassistant/components/hangouts/.translations/fr.json +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -5,17 +5,25 @@ "unknown": "Une erreur inconnue s'est produite" }, "error": { + "invalid_2fa": "Authentification \u00e0 2 facteurs invalide, veuillez r\u00e9essayer.", + "invalid_2fa_method": "M\u00e9thode 2FA non valide (v\u00e9rifiez sur le t\u00e9l\u00e9phone).", "invalid_login": "Login invalide, veuillez r\u00e9essayer." }, "step": { "2fa": { + "data": { + "2fa": "Code PIN d'authentification \u00e0 2 facteurs" + }, "title": "Authentification \u00e0 2 facteurs" }, "user": { "data": { + "email": "Adresse e-mail", "password": "Mot de passe" - } + }, + "title": "Connexion \u00e0 Google Hangouts" } - } + }, + "title": "Google Hangouts" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/he.json b/homeassistant/components/hangouts/.translations/he.json new file mode 100644 index 00000000000..28326d97142 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/he.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4." + }, + "error": { + "invalid_2fa": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d1\u05d1\u05e7\u05e9\u05d4 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "invalid_2fa_method": "\u05d3\u05e8\u05da \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea (\u05d0\u05de\u05ea \u05d1\u05d8\u05dc\u05e4\u05d5\u05df).", + "invalid_login": "\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." + }, + "step": { + "2fa": { + "data": { + "2fa": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9" + }, + "user": { + "data": { + "email": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc- Google Hangouts" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/hu.json b/homeassistant/components/hangouts/.translations/hu.json new file mode 100644 index 00000000000..2631843c784 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A Google Hangouts m\u00e1r konfigur\u00e1lva van", + "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt." + }, + "error": { + "invalid_2fa": "\u00c9rv\u00e9nytelen K\u00e9tfaktoros hiteles\u00edt\u00e9s, pr\u00f3b\u00e1ld \u00fajra.", + "invalid_2fa_method": "\u00c9rv\u00e9nytelen 2FA M\u00f3dszer (Ellen\u0151rz\u00e9s a Telefonon).", + "invalid_login": "\u00c9rv\u00e9nytelen bejelentkez\u00e9s, pr\u00f3b\u00e1ld \u00fajra." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA Pin" + }, + "title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s" + }, + "user": { + "data": { + "email": "E-Mail C\u00edm", + "password": "Jelsz\u00f3" + }, + "title": "Google Hangouts Bejelentkez\u00e9s" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/id.json b/homeassistant/components/hangouts/.translations/id.json new file mode 100644 index 00000000000..46a574bdf8a --- /dev/null +++ b/homeassistant/components/hangouts/.translations/id.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts sudah dikonfigurasikan", + "unknown": "Kesalahan tidak dikenal terjadi." + }, + "error": { + "invalid_2fa": "Autentikasi 2 Faktor Tidak Valid, silakan coba lagi.", + "invalid_2fa_method": "Metode 2FA Tidak Sah (Verifikasi di Ponsel).", + "invalid_login": "Login tidak valid, silahkan coba lagi." + }, + "step": { + "2fa": { + "data": { + "2fa": "Pin 2FA" + }, + "description": "Kosong", + "title": "2-Faktor-Otentikasi" + }, + "user": { + "data": { + "email": "Alamat email", + "password": "Kata sandi" + }, + "description": "Kosong", + "title": "Google Hangouts Login" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/nn.json b/homeassistant/components/hangouts/.translations/nn.json new file mode 100644 index 00000000000..58e5f4f45fd --- /dev/null +++ b/homeassistant/components/hangouts/.translations/nn.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts er allereie konfigurert", + "unknown": "Det hende ein ukjent feil" + }, + "error": { + "invalid_2fa": "Ugyldig to-faktor-autentisering. Ver vennleg og pr\u00f8v igjen.", + "invalid_2fa_method": "Ugyldig 2FA-metode (godkjenn p\u00e5 telefonen).", + "invalid_login": "Ugyldig innlogging. Pr\u00f8v igjen." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA PIN" + }, + "title": "To-faktor-autentiserin" + }, + "user": { + "data": { + "email": "Epostadresse", + "password": "Passord" + }, + "title": "Google Hangouts Login" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/pl.json b/homeassistant/components/hangouts/.translations/pl.json index a8314761f8d..5e0ecfa2900 100644 --- a/homeassistant/components/hangouts/.translations/pl.json +++ b/homeassistant/components/hangouts/.translations/pl.json @@ -14,6 +14,7 @@ "data": { "2fa": "PIN" }, + "description": "Pusty", "title": "Uwierzytelnianie dwusk\u0142adnikowe" }, "user": { @@ -21,6 +22,7 @@ "email": "Adres e-mail", "password": "Has\u0142o" }, + "description": "Pusty", "title": "Logowanie do Google Hangouts" } }, diff --git a/homeassistant/components/hangouts/.translations/pt-BR.json b/homeassistant/components/hangouts/.translations/pt-BR.json index 41b097f3f8d..00c533311fc 100644 --- a/homeassistant/components/hangouts/.translations/pt-BR.json +++ b/homeassistant/components/hangouts/.translations/pt-BR.json @@ -1,16 +1,25 @@ { "config": { "abort": { - "already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado." + "already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado.", + "unknown": "Ocorreu um erro desconhecido." + }, + "error": { + "invalid_2fa": "Autentica\u00e7\u00e3o de 2 fatores inv\u00e1lida, por favor, tente novamente.", + "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar no telefone).", + "invalid_login": "Login inv\u00e1lido, por favor, tente novamente." }, "step": { "2fa": { - "title": "" + "description": "Vazio", + "title": "Autentica\u00e7\u00e3o de 2 Fatores" }, "user": { "data": { + "email": "Endere\u00e7o de e-mail", "password": "Senha" }, + "description": "Vazio", "title": "Login do Hangouts do Google" } }, diff --git a/homeassistant/components/hangouts/.translations/sv.json b/homeassistant/components/hangouts/.translations/sv.json new file mode 100644 index 00000000000..90bf4e97712 --- /dev/null +++ b/homeassistant/components/hangouts/.translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Google Hangouts \u00e4r redan inst\u00e4llt", + "unknown": "Ett ok\u00e4nt fel intr\u00e4ffade" + }, + "error": { + "invalid_2fa": "Ogiltig 2FA autentisering, f\u00f6rs\u00f6k igen.", + "invalid_2fa_method": "Ogiltig 2FA-metod (Verifiera med telefon).", + "invalid_login": "Ogiltig inloggning, f\u00f6rs\u00f6k igen." + }, + "step": { + "2fa": { + "data": { + "2fa": "2FA Pinkod" + }, + "title": "Tv\u00e5faktorsautentisering" + }, + "user": { + "data": { + "email": "E-postadress", + "password": "L\u00f6senord" + }, + "title": "Google Hangouts-inloggning" + } + }, + "title": "Google Hangouts" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index ebadff57be3..8480ae09549 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -9,7 +9,9 @@ import logging import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.hangouts.intents import HelpIntent from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import intent from homeassistant.helpers import dispatcher import homeassistant.helpers.config_validation as cv @@ -18,12 +20,13 @@ from .const import ( EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE, SERVICE_UPDATE, CONF_SENTENCES, CONF_MATCHERS, - CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA) + CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA, + CONF_DEFAULT_CONVERSATIONS, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, + INTENT_HELP) # We need an import from .config_flow, without it .config_flow is never loaded. from .config_flow import HangoutsFlowHandler # noqa: F401 - REQUIREMENTS = ['hangups==0.4.5'] _LOGGER = logging.getLogger(__name__) @@ -33,6 +36,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_INTENTS, default={}): vol.Schema({ cv.string: INTENT_SCHEMA }), + vol.Optional(CONF_DEFAULT_CONVERSATIONS, default=[]): + [TARGETS_SCHEMA], vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]): [TARGETS_SCHEMA] }) @@ -47,16 +52,23 @@ async def async_setup(hass, config): if config is None: hass.data[DOMAIN] = { CONF_INTENTS: {}, + CONF_DEFAULT_CONVERSATIONS: [], CONF_ERROR_SUPPRESSED_CONVERSATIONS: [], } return True hass.data[DOMAIN] = { CONF_INTENTS: config[CONF_INTENTS], + CONF_DEFAULT_CONVERSATIONS: config[CONF_DEFAULT_CONVERSATIONS], CONF_ERROR_SUPPRESSED_CONVERSATIONS: config[CONF_ERROR_SUPPRESSED_CONVERSATIONS], } + if (hass.data[DOMAIN][CONF_INTENTS] and + INTENT_HELP not in hass.data[DOMAIN][CONF_INTENTS]): + hass.data[DOMAIN][CONF_INTENTS][INTENT_HELP] = { + CONF_SENTENCES: ['HELP']} + for data in hass.data[DOMAIN][CONF_INTENTS].values(): matchers = [] for sentence in data[CONF_SENTENCES]: @@ -82,6 +94,7 @@ async def async_setup_entry(hass, config): hass, config.data.get(CONF_REFRESH_TOKEN), hass.data[DOMAIN][CONF_INTENTS], + hass.data[DOMAIN][CONF_DEFAULT_CONVERSATIONS], hass.data[DOMAIN][CONF_ERROR_SUPPRESSED_CONVERSATIONS]) hass.data[DOMAIN][CONF_BOT] = bot except GoogleAuthError as exception: @@ -96,11 +109,12 @@ async def async_setup_entry(hass, config): dispatcher.async_dispatcher_connect( hass, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - bot.async_update_conversation_commands) + bot.async_resolve_conversations) + dispatcher.async_dispatcher_connect( hass, - EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - bot.async_handle_update_error_suppressed_conversations) + EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, + bot.async_update_conversation_commands) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, bot.async_handle_hass_stop) @@ -116,6 +130,8 @@ async def async_setup_entry(hass, config): async_handle_update_users_and_conversations, schema=vol.Schema({})) + intent.async_register(hass, HelpIntent(hass)) + return True diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index 74eb14b050d..9d66338dff0 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure Google Hangouts.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback @@ -19,10 +19,11 @@ def configured_hangouts(hass): @config_entries.HANDLERS.register(HANGOUTS_DOMAIN) -class HangoutsFlowHandler(data_entry_flow.FlowHandler): +class HangoutsFlowHandler(config_entries.ConfigFlow): """Config flow Google Hangouts.""" VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH def __init__(self): """Initialize Google Hangouts config flow.""" diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py index 3b96edf93a2..5a527fae260 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -3,7 +3,8 @@ import logging import voluptuous as vol -from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET +from homeassistant.components.notify \ + import ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger('homeassistant.components.hangouts') @@ -24,10 +25,13 @@ CONF_INTENT_TYPE = 'intent_type' CONF_SENTENCES = 'sentences' CONF_MATCHERS = 'matchers' +INTENT_HELP = 'HangoutsHelp' + EVENT_HANGOUTS_CONNECTED = 'hangouts_connected' EVENT_HANGOUTS_DISCONNECTED = 'hangouts_disconnected' EVENT_HANGOUTS_USERS_CHANGED = 'hangouts_users_changed' EVENT_HANGOUTS_CONVERSATIONS_CHANGED = 'hangouts_conversations_changed' +EVENT_HANGOUTS_CONVERSATIONS_RESOLVED = 'hangouts_conversations_resolved' EVENT_HANGOUTS_MESSAGE_RECEIVED = 'hangouts_message_received' CONF_CONVERSATION_ID = 'id' @@ -53,10 +57,15 @@ MESSAGE_SEGMENT_SCHEMA = vol.Schema({ vol.Optional('parse_str'): cv.boolean, vol.Optional('link_target'): cv.string }) +MESSAGE_DATA_SCHEMA = vol.Schema({ + vol.Optional('image_file'): cv.string, + vol.Optional('image_url'): cv.string +}) MESSAGE_SCHEMA = vol.Schema({ vol.Required(ATTR_TARGET): [TARGETS_SCHEMA], - vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA] + vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA], + vol.Optional(ATTR_DATA): MESSAGE_DATA_SCHEMA }) INTENT_SCHEMA = vol.All( diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 15f4156d374..8747bff9ba7 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,14 +1,17 @@ """The Hangouts Bot.""" +import io import logging - +import asyncio +import aiohttp +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import dispatcher, intent from .const import ( - ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, DOMAIN, + ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA, CONF_CONVERSATIONS, DOMAIN, EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED, CONF_MATCHERS, CONF_CONVERSATION_ID, - CONF_CONVERSATION_NAME) + CONF_CONVERSATION_NAME, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, INTENT_HELP) _LOGGER = logging.getLogger(__name__) @@ -16,7 +19,8 @@ _LOGGER = logging.getLogger(__name__) class HangoutsBot: """The Hangouts Bot.""" - def __init__(self, hass, refresh_token, intents, error_suppressed_convs): + def __init__(self, hass, refresh_token, intents, + default_convs, error_suppressed_convs): """Set up the client.""" self.hass = hass self._connected = False @@ -29,6 +33,8 @@ class HangoutsBot: self._client = None self._user_list = None self._conversation_list = None + self._default_convs = default_convs + self._default_conv_ids = None self._error_suppressed_convs = error_suppressed_convs self._error_suppressed_conv_ids = None @@ -51,7 +57,7 @@ class HangoutsBot: return conv return None - def async_update_conversation_commands(self, _): + def async_update_conversation_commands(self): """Refresh the commands for every conversation.""" self._conversation_intents = {} @@ -63,6 +69,8 @@ class HangoutsBot: if conv_id is not None: conversations.append(conv_id) data['_' + CONF_CONVERSATIONS] = conversations + elif self._default_conv_ids: + data['_' + CONF_CONVERSATIONS] = self._default_conv_ids else: data['_' + CONF_CONVERSATIONS] = \ [conv.id_ for conv in self._conversation_list.get_all()] @@ -81,13 +89,22 @@ class HangoutsBot: self._conversation_list.on_event.add_observer( self._async_handle_conversation_event) - def async_handle_update_error_suppressed_conversations(self, _): - """Resolve the list of error suppressed conversations.""" + def async_resolve_conversations(self, _): + """Resolve the list of default and error suppressed conversations.""" + self._default_conv_ids = [] self._error_suppressed_conv_ids = [] + + for conversation in self._default_convs: + conv_id = self._resolve_conversation_id(conversation) + if conv_id is not None: + self._default_conv_ids.append(conv_id) + for conversation in self._error_suppressed_convs: conv_id = self._resolve_conversation_id(conversation) if conv_id is not None: self._error_suppressed_conv_ids.append(conv_id) + dispatcher.async_dispatcher_send(self.hass, + EVENT_HANGOUTS_CONVERSATIONS_RESOLVED) async def _async_handle_conversation_event(self, event): from hangups import ChatMessageEvent @@ -112,7 +129,8 @@ class HangoutsBot: if intents is not None: is_error = False try: - intent_result = await self._async_process(intents, message) + intent_result = await self._async_process(intents, message, + conv_id) except (intent.UnknownIntent, intent.IntentHandleError) as err: is_error = True intent_result = intent.IntentResponse() @@ -131,9 +149,10 @@ class HangoutsBot: is_error and conv_id in self._error_suppressed_conv_ids): await self._async_send_message( [{'text': message, 'parse_str': True}], - [{CONF_CONVERSATION_ID: conv_id}]) + [{CONF_CONVERSATION_ID: conv_id}], + None) - async def _async_process(self, intents, text): + async def _async_process(self, intents, text, conv_id): """Detect a matching intent.""" for intent_type, data in intents.items(): for matcher in data.get(CONF_MATCHERS, []): @@ -141,12 +160,15 @@ class HangoutsBot: if not match: continue + if intent_type == INTENT_HELP: + return await self.hass.helpers.intent.async_handle( + DOMAIN, intent_type, + {'conv_id': {'value': conv_id}}, text) - response = await self.hass.helpers.intent.async_handle( + return await self.hass.helpers.intent.async_handle( DOMAIN, intent_type, - {key: {'value': value} for key, value - in match.groupdict().items()}, text) - return response + {key: {'value': value} + for key, value in match.groupdict().items()}, text) async def async_connect(self): """Login to the Google Hangouts.""" @@ -185,7 +207,7 @@ class HangoutsBot: """Run once when Home Assistant stops.""" await self.async_disconnect() - async def _async_send_message(self, message, targets): + async def _async_send_message(self, message, targets, data): conversations = [] for target in targets: conversation = None @@ -204,20 +226,59 @@ class HangoutsBot: from hangups import ChatMessageSegment, hangouts_pb2 messages = [] for segment in message: + if messages: + messages.append(ChatMessageSegment('', + segment_type=hangouts_pb2. + SEGMENT_TYPE_LINE_BREAK)) if 'parse_str' in segment and segment['parse_str']: messages.extend(ChatMessageSegment.from_str(segment['text'])) else: if 'parse_str' in segment: del segment['parse_str'] messages.append(ChatMessageSegment(**segment)) - messages.append(ChatMessageSegment('', - segment_type=hangouts_pb2. - SEGMENT_TYPE_LINE_BREAK)) + + image_file = None + if data: + if data.get('image_url'): + uri = data.get('image_url') + try: + websession = async_get_clientsession(self.hass) + async with websession.get(uri, timeout=5) as response: + if response.status != 200: + _LOGGER.error( + 'Fetch image failed, %s, %s', + response.status, + response + ) + image_file = None + else: + image_data = await response.read() + image_file = io.BytesIO(image_data) + image_file.name = "image.png" + except (asyncio.TimeoutError, aiohttp.ClientError) as error: + _LOGGER.error( + 'Failed to fetch image, %s', + type(error) + ) + image_file = None + elif data.get('image_file'): + uri = data.get('image_file') + if self.hass.config.is_allowed_path(uri): + try: + image_file = open(uri, 'rb') + except IOError as error: + _LOGGER.error( + 'Image file I/O error(%s): %s', + error.errno, + error.strerror + ) + else: + _LOGGER.error('Path "%s" not allowed', uri) if not messages: return False for conv in conversations: - await conv.send_message(messages) + await conv.send_message(messages, image_file) async def _async_list_conversations(self): import hangups @@ -242,8 +303,13 @@ class HangoutsBot: async def async_handle_send_message(self, service): """Handle the send_message service.""" await self._async_send_message(service.data[ATTR_MESSAGE], - service.data[ATTR_TARGET]) + service.data[ATTR_TARGET], + service.data[ATTR_DATA]) async def async_handle_update_users_and_conversations(self, _=None): """Handle the update_users_and_conversations service.""" await self._async_list_conversations() + + def get_intents(self, conv_id): + """Return the intents for a specific conversation.""" + return self._conversation_intents.get(conv_id) diff --git a/homeassistant/components/hangouts/intents.py b/homeassistant/components/hangouts/intents.py new file mode 100644 index 00000000000..be52f059139 --- /dev/null +++ b/homeassistant/components/hangouts/intents.py @@ -0,0 +1,33 @@ +"""Intents for the hangouts component.""" +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv + +from .const import INTENT_HELP, DOMAIN, CONF_BOT + + +class HelpIntent(intent.IntentHandler): + """Handle Help intents.""" + + intent_type = INTENT_HELP + slot_schema = { + 'conv_id': cv.string + } + + def __init__(self, hass): + """Set up the intent.""" + self.hass = hass + + async def async_handle(self, intent_obj): + """Handle the intent.""" + slots = self.async_validate_slots(intent_obj.slots) + conv_id = slots['conv_id']['value'] + + intents = self.hass.data[DOMAIN][CONF_BOT].get_intents(conv_id) + response = intent_obj.create_response() + help_text = "I understand the following sentences:" + for intent_data in intents.values(): + for sentence in intent_data['sentences']: + help_text += "\n'{}'".format(sentence) + response.async_set_speech(help_text) + + return response diff --git a/homeassistant/components/hangouts/services.yaml b/homeassistant/components/hangouts/services.yaml index 5d314bc2479..ded324d2de9 100644 --- a/homeassistant/components/hangouts/services.yaml +++ b/homeassistant/components/hangouts/services.yaml @@ -9,4 +9,8 @@ send_message: example: '[{"id": "UgxrXzVrARmjx_C6AZx4AaABAagBo-6UCw"}, {"name": "Test Conversation"}]' message: description: List of message segments, only the "text" field is required in every segment. [Required] - example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' \ No newline at end of file + example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' + data: + description: Other options ['image_file' / 'image_url'] + example: '{ "image_file": "file" }' or '{ "image_url": "url" }' + diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json index dd421fee57a..c83a0ae0876 100644 --- a/homeassistant/components/hangouts/strings.json +++ b/homeassistant/components/hangouts/strings.json @@ -15,14 +15,12 @@ "email": "E-Mail Address", "password": "Password" }, - "description": "", "title": "Google Hangouts Login" }, "2fa": { "data": { "2fa": "2FA Pin" }, - "description": "", "title": "2-Factor-Authentication" } }, diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index c51d45cc339..55cc7f54787 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -22,23 +22,24 @@ _LOGGER = logging.getLogger(__name__) X_HASSIO = 'X-HASSIO-KEY' -NO_TIMEOUT = { - re.compile(r'^homeassistant/update$'), - re.compile(r'^host/update$'), - re.compile(r'^supervisor/update$'), - re.compile(r'^addons/[^/]*/update$'), - re.compile(r'^addons/[^/]*/install$'), - re.compile(r'^addons/[^/]*/rebuild$'), - re.compile(r'^snapshots/.*/full$'), - re.compile(r'^snapshots/.*/partial$'), - re.compile(r'^snapshots/[^/]*/upload$'), - re.compile(r'^snapshots/[^/]*/download$'), -} +NO_TIMEOUT = re.compile( + r'^(?:' + r'|homeassistant/update' + r'|host/update' + r'|supervisor/update' + r'|addons/[^/]+/(?:update|install|rebuild)' + r'|snapshots/.+/full' + r'|snapshots/.+/partial' + r'|snapshots/[^/]+/(?:upload|download)' + r')$' +) -NO_AUTH = { - re.compile(r'^app/.*$'), - re.compile(r'^addons/[^/]*/logo$') -} +NO_AUTH = re.compile( + r'^(?:' + r'|app/.*' + r'|addons/[^/]+/logo' + r')$' +) class HassIOView(HomeAssistantView): @@ -128,15 +129,13 @@ def _create_response_log(client, data): def _get_timeout(path): """Return timeout for a URL path.""" - for re_path in NO_TIMEOUT: - if re_path.match(path): - return 0 + if NO_TIMEOUT.match(path): + return 0 return 300 def _need_auth(path): """Return if a path need authentication.""" - for re_path in NO_AUTH: - if re_path.match(path): - return False + if NO_AUTH.match(path): + return False return True diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index eac02855b0b..8c12243ee8f 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -22,9 +22,9 @@ from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, - CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, - DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, - TYPE_OUTLET, TYPE_SWITCH) + CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, + SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH) from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) @@ -150,6 +150,8 @@ def get_accessory(hass, driver, state, aid, config): elif device_class == DEVICE_CLASS_PM25 \ or DEVICE_CLASS_PM25 in state.entity_id: a_type = 'AirQualitySensor' + elif device_class == DEVICE_CLASS_CO: + a_type = 'CarbonMonoxideSensor' elif device_class == DEVICE_CLASS_CO2 \ or DEVICE_CLASS_CO2 in state.entity_id: a_type = 'CarbonDioxideSensor' diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 33d2c0bfb85..df488d4a73a 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -69,6 +69,8 @@ CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' +CHAR_CARBON_MONOXIDE_LEVEL = 'CarbonMonoxideLevel' +CHAR_CARBON_MONOXIDE_PEAK_LEVEL = 'CarbonMonoxidePeakLevel' CHAR_CHARGING_STATE = 'ChargingState' CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' @@ -114,6 +116,7 @@ PROP_MIN_VALUE = 'minValue' PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} # #### Device Classes #### +DEVICE_CLASS_CO = 'co' DEVICE_CLASS_CO2 = 'co2' DEVICE_CLASS_DOOR = 'door' DEVICE_CLASS_GARAGE_DOOR = 'garage_door' @@ -125,3 +128,7 @@ DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' DEVICE_CLASS_WINDOW = 'window' + +# #### Thresholds #### +THRESHOLD_CO = 25 +THRESHOLD_CO2 = 1000 diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index d4c2cb58209..d2101b1e6f9 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -13,6 +13,7 @@ from .const import ( CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED, + CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL, CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, @@ -23,7 +24,7 @@ from .const import ( SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR, SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR, SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, - SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR) + SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR, THRESHOLD_CO, THRESHOLD_CO2) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -114,6 +115,34 @@ class AirQualitySensor(HomeAccessory): _LOGGER.debug('%s: Set to %d', self.entity_id, density) +@TYPES.register('CarbonMonoxideSensor') +class CarbonMonoxideSensor(HomeAccessory): + """Generate a CarbonMonoxidSensor accessory as CO sensor.""" + + def __init__(self, *args): + """Initialize a CarbonMonoxideSensor accessory object.""" + super().__init__(*args, category=CATEGORY_SENSOR) + + serv_co = self.add_preload_service(SERV_CARBON_MONOXIDE_SENSOR, [ + CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL]) + self.char_level = serv_co.configure_char( + CHAR_CARBON_MONOXIDE_LEVEL, value=0) + self.char_peak = serv_co.configure_char( + CHAR_CARBON_MONOXIDE_PEAK_LEVEL, value=0) + self.char_detected = serv_co.configure_char( + CHAR_CARBON_MONOXIDE_DETECTED, value=0) + + def update_state(self, new_state): + """Update accessory after state change.""" + value = convert_to_float(new_state.state) + if value: + self.char_level.set_value(value) + if value > self.char_peak.value: + self.char_peak.set_value(value) + self.char_detected.set_value(value > THRESHOLD_CO) + _LOGGER.debug('%s: Set to %d', self.entity_id, value) + + @TYPES.register('CarbonDioxideSensor') class CarbonDioxideSensor(HomeAccessory): """Generate a CarbonDioxideSensor accessory as CO2 sensor.""" @@ -124,7 +153,7 @@ class CarbonDioxideSensor(HomeAccessory): serv_co2 = self.add_preload_service(SERV_CARBON_DIOXIDE_SENSOR, [ CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL]) - self.char_co2 = serv_co2.configure_char( + self.char_level = serv_co2.configure_char( CHAR_CARBON_DIOXIDE_LEVEL, value=0) self.char_peak = serv_co2.configure_char( CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0) @@ -133,13 +162,13 @@ class CarbonDioxideSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" - co2 = convert_to_float(new_state.state) - if co2: - self.char_co2.set_value(co2) - if co2 > self.char_peak.value: - self.char_peak.set_value(co2) - self.char_detected.set_value(co2 > 1000) - _LOGGER.debug('%s: Set to %d', self.entity_id, co2) + value = convert_to_float(new_state.state) + if value: + self.char_level.set_value(value) + if value > self.char_peak.value: + self.char_peak.set_value(value) + self.char_detected.set_value(value > THRESHOLD_CO2) + _LOGGER.debug('%s: Set to %d', self.entity_id, value) @TYPES.register('LightSensor') diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 53c8e267016..3d6eb69bb5e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -18,9 +18,8 @@ from homeassistant.const import ( from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.47'] +REQUIREMENTS = ['pyhomematic==0.1.49'] _LOGGER = logging.getLogger(__name__) @@ -77,7 +76,8 @@ HM_DEVICE_TYPES = { 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor', - 'IPKeySwitchPowermeter'], + 'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus', + 'IPWeatherSensorBasic'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -87,7 +87,7 @@ HM_DEVICE_TYPES = { 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor', - 'SmartwareMotion'], + 'SmartwareMotion', 'IPWeatherSensorPlus'], DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], DISCOVER_LOCKS: ['KeyMatic'] } @@ -107,6 +107,7 @@ HM_ATTRIBUTE_SUPPORT = { 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'RSSI_PEER': ['rssi', {}], + 'RSSI_DEVICE': ['rssi', {}], 'VALVE_STATE': ['valve', {}], 'BATTERY_STATE': ['battery', {}], 'CONTROL_MODE': ['mode', { @@ -243,78 +244,6 @@ SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema({ }) -@bind_hass -def virtualkey(hass, address, channel, param, interface=None): - """Send virtual keypress to homematic controller.""" - data = { - ATTR_ADDRESS: address, - ATTR_CHANNEL: channel, - ATTR_PARAM: param, - ATTR_INTERFACE: interface, - } - - hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data) - - -@bind_hass -def set_variable_value(hass, entity_id, value): - """Change value of a Homematic system variable.""" - data = { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - } - - hass.services.call(DOMAIN, SERVICE_SET_VARIABLE_VALUE, data) - - -@bind_hass -def set_device_value(hass, address, channel, param, value, interface=None): - """Call setValue XML-RPC method of supplied interface.""" - data = { - ATTR_ADDRESS: address, - ATTR_CHANNEL: channel, - ATTR_PARAM: param, - ATTR_VALUE: value, - ATTR_INTERFACE: interface, - } - - hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data) - - -@bind_hass -def put_paramset(hass, interface, address, paramset_key, paramset): - """Call putParamset XML-RPC method of supplied interface.""" - data = { - ATTR_INTERFACE: interface, - ATTR_ADDRESS: address, - ATTR_PARAMSET_KEY: paramset_key, - ATTR_PARAMSET: paramset, - } - - hass.services.call(DOMAIN, SERVICE_PUT_PARAMSET, data) - - -@bind_hass -def set_install_mode(hass, interface, mode=None, time=None, address=None): - """Call setInstallMode XML-RPC method of supplied interface.""" - data = { - key: value for key, value in ( - (ATTR_INTERFACE, interface), - (ATTR_MODE, mode), - (ATTR_TIME, time), - (ATTR_ADDRESS, address) - ) if value - } - - hass.services.call(DOMAIN, SERVICE_SET_INSTALL_MODE, data) - - -@bind_hass -def reconnect(hass): - """Reconnect to CCU/Homegear.""" - hass.services.call(DOMAIN, SERVICE_RECONNECT, {}) - - def setup(hass, config): """Set up the Homematic component.""" from pyhomematic import HMConnection diff --git a/homeassistant/components/homematicip_cloud/.translations/ca.json b/homeassistant/components/homematicip_cloud/.translations/ca.json index aab974ba137..7cc5943b830 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ca.json +++ b/homeassistant/components/homematicip_cloud/.translations/ca.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "El punt d'acc\u00e9s ja est\u00e0 configurat", - "conection_aborted": "No s'ha pogut connectar al servidor HMIP", "connection_aborted": "No s'ha pogut connectar al servidor HMIP", "unknown": "S'ha produ\u00eft un error desconegut." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/cs.json b/homeassistant/components/homematicip_cloud/.translations/cs.json index 4030450e51c..fa98029f6b0 100644 --- a/homeassistant/components/homematicip_cloud/.translations/cs.json +++ b/homeassistant/components/homematicip_cloud/.translations/cs.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "P\u0159\u00edstupov\u00fd bod je ji\u017e nakonfigurov\u00e1n", - "conection_aborted": "Nelze se p\u0159ipojit k serveru HMIP", "connection_aborted": "Nelze se p\u0159ipojit k HMIP serveru", "unknown": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b" }, diff --git a/homeassistant/components/homematicip_cloud/.translations/da.json b/homeassistant/components/homematicip_cloud/.translations/da.json index b617130945a..7473b4a7b86 100644 --- a/homeassistant/components/homematicip_cloud/.translations/da.json +++ b/homeassistant/components/homematicip_cloud/.translations/da.json @@ -2,6 +2,13 @@ "config": { "error": { "invalid_pin": "Ugyldig PIN, pr\u00f8v igen." + }, + "step": { + "init": { + "data": { + "pin": "Pin kode (valgfri)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/de.json b/homeassistant/components/homematicip_cloud/.translations/de.json index fdccac0d229..bd600f7d2ef 100644 --- a/homeassistant/components/homematicip_cloud/.translations/de.json +++ b/homeassistant/components/homematicip_cloud/.translations/de.json @@ -2,15 +2,14 @@ "config": { "abort": { "already_configured": "Der Accesspoint ist bereits konfiguriert", - "conection_aborted": "Keine Verbindung zum HMIP-Server m\u00f6glich", "connection_aborted": "Konnte nicht mit HMIP Server verbinden", "unknown": "Ein unbekannter Fehler ist aufgetreten." }, "error": { - "invalid_pin": "Ung\u00fcltige PIN, bitte versuchen Sie es erneut.", - "press_the_button": "Bitte dr\u00fccken Sie die blaue Taste.", - "register_failed": "Registrierung fehlgeschlagen, bitte versuchen Sie es erneut.", - "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuchen Sie es erneut." + "invalid_pin": "Ung\u00fcltige PIN, bitte versuche es erneut.", + "press_the_button": "Bitte dr\u00fccke die blaue Taste.", + "register_failed": "Registrierung fehlgeschlagen, bitte versuche es erneut.", + "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuche es erneut." }, "step": { "init": { @@ -22,7 +21,7 @@ "title": "HometicIP Accesspoint ausw\u00e4hlen" }, "link": { - "description": "Dr\u00fccken Sie den blauen Taster auf dem Accesspoint, sowie den Senden Button um HomematicIP mit Home Assistant zu verbinden.\n\n![Position des Tasters auf dem AP](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Dr\u00fccke den blauen Taster auf dem Accesspoint, sowie den Senden Button um HomematicIP mit Home Assistant zu verbinden.\n\n![Position des Tasters auf dem AP](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Verkn\u00fcpfe den Accesspoint" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/en.json b/homeassistant/components/homematicip_cloud/.translations/en.json index 6fcfcddd75d..605bb0d250b 100644 --- a/homeassistant/components/homematicip_cloud/.translations/en.json +++ b/homeassistant/components/homematicip_cloud/.translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Access point is already configured", - "conection_aborted": "Could not connect to HMIP server", "connection_aborted": "Could not connect to HMIP server", "unknown": "Unknown error occurred." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json index e15d0dbae64..8675d6e12b1 100644 --- a/homeassistant/components/homematicip_cloud/.translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Accesspoint ya est\u00e1 configurado", - "conection_aborted": "No se pudo conectar al servidor HMIP", "connection_aborted": "No se pudo conectar al servidor HMIP", "unknown": "Se produjo un error desconocido." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/fr.json b/homeassistant/components/homematicip_cloud/.translations/fr.json index c10cb519133..0e724d62bbe 100644 --- a/homeassistant/components/homematicip_cloud/.translations/fr.json +++ b/homeassistant/components/homematicip_cloud/.translations/fr.json @@ -1,12 +1,15 @@ { "config": { "abort": { - "unknown": "Une erreur inconnue s'est produite" + "already_configured": "Le point d'acc\u00e8s est d\u00e9j\u00e0 configur\u00e9", + "connection_aborted": "Impossible de se connecter au serveur HMIP", + "unknown": "Une erreur inconnue s'est produite." }, "error": { "invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.", "press_the_button": "Veuillez appuyer sur le bouton bleu.", - "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer." + "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer.", + "timeout_button": "D\u00e9lai d'attente expir\u00e9, veuillez r\u00e9\u00e9ssayer." }, "step": { "init": { @@ -14,8 +17,14 @@ "hapid": "ID du point d'acc\u00e8s (SGTIN)", "name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les p\u00e9riph\u00e9riques)", "pin": "Code PIN (facultatif)" - } + }, + "title": "Choisissez le point d'acc\u00e8s HomematicIP" + }, + "link": { + "description": "Appuyez sur le bouton bleu du point d'acc\u00e8s et sur le bouton Envoyer pour enregistrer HomematicIP avec Home Assistant. \n\n ![Emplacement du bouton sur le pont](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Lier le point d'acc\u00e8s" } - } + }, + "title": "HomematicIP Cloud" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/he.json b/homeassistant/components/homematicip_cloud/.translations/he.json index bdf1e436bad..c60294e21d5 100644 --- a/homeassistant/components/homematicip_cloud/.translations/he.json +++ b/homeassistant/components/homematicip_cloud/.translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05d2\u05d9\u05e9\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea", - "conection_aborted": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e8\u05ea HMIP", + "connection_aborted": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e8\u05ea HMIP", "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4." }, "error": { diff --git a/homeassistant/components/homematicip_cloud/.translations/id.json b/homeassistant/components/homematicip_cloud/.translations/id.json new file mode 100644 index 00000000000..0487434274c --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Jalur akses sudah dikonfigurasi", + "connection_aborted": "Tidak dapat terhubung ke server HMIP", + "unknown": "Kesalahan tidak dikenal terjadi." + }, + "error": { + "invalid_pin": "PIN tidak valid, silakan coba lagi.", + "press_the_button": "Silakan tekan tombol biru.", + "register_failed": "Gagal mendaftar, silakan coba lagi.", + "timeout_button": "Batas waktu tekan tombol biru berakhir, silakan coba lagi." + }, + "step": { + "init": { + "data": { + "hapid": "Titik akses ID (SGTIN)", + "name": "Nama (opsional, digunakan sebagai awalan nama untuk semua perangkat)", + "pin": "Kode Pin (opsional)" + }, + "title": "Pilih HomematicIP Access point" + }, + "link": { + "description": "Tekan tombol biru pada access point dan tombol submit untuk mendaftarkan HomematicIP dengan rumah asisten.\n\n! [Lokasi tombol di bridge] (/ static/images/config_flows/config_homematicip_cloud.png)", + "title": "Tautkan jalur akses" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/it.json b/homeassistant/components/homematicip_cloud/.translations/it.json index 95e600e6d03..9ef1abd500c 100644 --- a/homeassistant/components/homematicip_cloud/.translations/it.json +++ b/homeassistant/components/homematicip_cloud/.translations/it.json @@ -5,6 +5,7 @@ "connection_aborted": "Impossibile connettersi al server HMIP" }, "error": { + "invalid_pin": "PIN non valido, riprova.", "press_the_button": "Si prega di premere il pulsante blu.", "register_failed": "Registrazione fallita, si prega di riprovare." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/ja.json b/homeassistant/components/homematicip_cloud/.translations/ja.json index 105a7415789..6a03f3ec76b 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ja.json +++ b/homeassistant/components/homematicip_cloud/.translations/ja.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u306f\u65e2\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "conection_aborted": "HMIP\u30b5\u30fc\u30d0\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "error": { diff --git a/homeassistant/components/homematicip_cloud/.translations/ko.json b/homeassistant/components/homematicip_cloud/.translations/ko.json index 617b65ff623..7b8dc8b5087 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ko.json +++ b/homeassistant/components/homematicip_cloud/.translations/ko.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "conection_aborted": "HMIP \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "connection_aborted": "HMIP \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, @@ -22,7 +21,7 @@ "title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd" }, "link": { - "description": "Home Assistant\uc5d0 HomematicIP\ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \uc11c\ubc0b \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Home Assistant \uc5d0 HomematicIP \ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \uc11c\ubc0b \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", "title": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc5d0 \uc5f0\uacb0" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/lb.json b/homeassistant/components/homematicip_cloud/.translations/lb.json index a21767fc7d9..2cad909a7ee 100644 --- a/homeassistant/components/homematicip_cloud/.translations/lb.json +++ b/homeassistant/components/homematicip_cloud/.translations/lb.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Acesspoint ass schon konfigur\u00e9iert", - "conection_aborted": "Konnt sech net mam HMIP Server verbannen", "connection_aborted": "Konnt sech net mam HMIP Server verbannen", "unknown": "Onbekannten Feeler opgetrueden" }, diff --git a/homeassistant/components/homematicip_cloud/.translations/nl.json b/homeassistant/components/homematicip_cloud/.translations/nl.json index 40d1ced5007..ff3e2dea2cd 100644 --- a/homeassistant/components/homematicip_cloud/.translations/nl.json +++ b/homeassistant/components/homematicip_cloud/.translations/nl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Accesspoint is al geconfigureerd", - "conection_aborted": "Kon geen verbinding maken met de HMIP-server", "connection_aborted": "Kon geen verbinding maken met de HMIP-server", "unknown": "Er is een onbekende fout opgetreden." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/nn.json b/homeassistant/components/homematicip_cloud/.translations/nn.json new file mode 100644 index 00000000000..966c827c89d --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/nn.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Tilgangspunktet er allereie konfigurert", + "connection_aborted": "Kunne ikkje kople til HMIP-serveren", + "unknown": "Det hende ein ukjent feil." + }, + "error": { + "invalid_pin": "Ugyldig PIN. Pr\u00f8v igjen.", + "press_the_button": "Ver vennleg og trykk p\u00e5 den bl\u00e5 knappen.", + "register_failed": "Kunne ikkje registrere. Pr\u00f8v igjen.", + "timeout_button": "TIda gjekk ut for \u00e5 trykke p\u00e5 den bl\u00e5 knappen. Ver vennleg og pr\u00f8v igjen." + }, + "step": { + "init": { + "data": { + "hapid": "TilgangspunktID (SGTIN)", + "name": "Namn (valfrii. Brukt som namnprefiks for alle einingar)", + "pin": "Pinkode (valfritt)" + }, + "title": "Vel HomematicIP tilgangspunkt" + }, + "link": { + "description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og sendknappen for \u00e5 registrere HomematicIP med Home Assitant.\n\n ! [Plassering av knapp p\u00e5 bro] (/ static / images / config_flows / config_homematicip_cloud.png)", + "title": "Link tilgangspunk" + } + }, + "title": "HomematicIP Cloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/no.json b/homeassistant/components/homematicip_cloud/.translations/no.json index a310a918f64..d9e6636c972 100644 --- a/homeassistant/components/homematicip_cloud/.translations/no.json +++ b/homeassistant/components/homematicip_cloud/.translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Tilgangspunktet er allerede konfigurert", - "conection_aborted": "Kunne ikke koble til HMIP serveren", "connection_aborted": "Kunne ikke koble til HMIP serveren", "unknown": "Ukjent feil oppstod." }, @@ -22,7 +21,7 @@ "title": "Velg HomematicIP tilgangspunkt" }, "link": { - "description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Trykk p\u00e5 den bl\u00e5 knappen p\u00e5 tilgangspunktet og p\u00e5 send knappen for \u00e5 registrere HomematicIP med Home Assistant. \n\n![Plassering av knapp p\u00e5 bridge](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Link tilgangspunkt" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/pl.json b/homeassistant/components/homematicip_cloud/.translations/pl.json index 3fcbe7e69d1..7c8714c2c11 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pl.json +++ b/homeassistant/components/homematicip_cloud/.translations/pl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany", - "conection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP", "connection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, diff --git a/homeassistant/components/homematicip_cloud/.translations/pt-BR.json b/homeassistant/components/homematicip_cloud/.translations/pt-BR.json index d4ecbe50107..82166a1aaaf 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pt-BR.json +++ b/homeassistant/components/homematicip_cloud/.translations/pt-BR.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "O Accesspoint j\u00e1 est\u00e1 configurado", - "conection_aborted": "N\u00e3o foi poss\u00edvel conectar ao servidor HMIP", "connection_aborted": "N\u00e3o foi poss\u00edvel conectar ao servidor HMIP", "unknown": "Ocorreu um erro desconhecido." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/pt.json b/homeassistant/components/homematicip_cloud/.translations/pt.json index 87ee494a875..18377490a5f 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pt.json +++ b/homeassistant/components/homematicip_cloud/.translations/pt.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "O ponto de acesso j\u00e1 se encontra configurado", - "conection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP", "connection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP", "unknown": "Ocorreu um erro desconhecido." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index ed42daf19cd..ef2b3be4a64 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u0422\u043e\u0447\u043a\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", - "conection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, diff --git a/homeassistant/components/homematicip_cloud/.translations/sl.json b/homeassistant/components/homematicip_cloud/.translations/sl.json index 4c4a00e31e0..eabb31ac833 100644 --- a/homeassistant/components/homematicip_cloud/.translations/sl.json +++ b/homeassistant/components/homematicip_cloud/.translations/sl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Dostopna to\u010dka je \u017ee konfigurirana", - "conection_aborted": "Povezave s stre\u017enikom HMIP ni bila mogo\u010da", "connection_aborted": "Povezava s stre\u017enikom HMIP ni bila mogo\u010da", "unknown": "Pri\u0161lo je do neznane napake" }, diff --git a/homeassistant/components/homematicip_cloud/.translations/sv.json b/homeassistant/components/homematicip_cloud/.translations/sv.json index 945dca8a277..da6bde77ae3 100644 --- a/homeassistant/components/homematicip_cloud/.translations/sv.json +++ b/homeassistant/components/homematicip_cloud/.translations/sv.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Accesspunkten \u00e4r redan konfigurerad", - "conection_aborted": "Kunde inte ansluta till HMIP server", + "connection_aborted": "Det gick inte att ansluta till HMIP-servern", "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" }, "error": { diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json index 930b649bceb..629ee4347fe 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hans.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u63a5\u5165\u70b9\u5df2\u7ecf\u914d\u7f6e\u5b8c\u6210", - "conection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668", "connection_aborted": "\u65e0\u6cd5\u8fde\u63a5\u5230 HMIP \u670d\u52a1\u5668", "unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002" }, diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json index 9340070d9a3..d2d33455191 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Accesspoint \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "conection_aborted": "\u7121\u6cd5\u9023\u7dda\u81f3 HMIP \u4f3a\u670d\u5668", "connection_aborted": "\u7121\u6cd5\u9023\u7dda\u81f3 HMIP \u4f3a\u670d\u5668", "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index d5045cf151b..ea251a3bf87 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the HomematicIP Cloud component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.core import callback from .const import DOMAIN as HMIPC_DOMAIN @@ -18,10 +18,11 @@ def configured_haps(hass): @config_entries.HANDLERS.register(HMIPC_DOMAIN) -class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler): +class HomematicipCloudFlowHandler(config_entries.ConfigFlow): """Config flow for the HomematicIP Cloud component.""" VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH def __init__(self): """Initialize HomematicIP Cloud config flow.""" diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 9c335befda4..6a4f958387e 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -61,6 +61,11 @@ class HomematicipGenericDevice(Entity): """Device available.""" return not self._device.unreach + @property + def unique_id(self): + """Return a unique ID.""" + return "{}_{}".format(self.__class__.__name__, self._device.id) + @property def icon(self): """Return the icon.""" diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 015c386e836..2a25de96edc 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -10,7 +10,6 @@ from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import persistent_notification from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -92,9 +91,10 @@ async def process_wrong_login(request): msg = ('Login attempt or request with invalid authentication ' 'from {}'.format(remote_addr)) _LOGGER.warning(msg) - persistent_notification.async_create( - request.app['hass'], msg, 'Login attempt failed', - NOTIFICATION_ID_LOGIN) + + hass = request.app['hass'] + hass.components.persistent_notification.async_create( + msg, 'Login attempt failed', NOTIFICATION_ID_LOGIN) # Check if ban middleware is loaded if (KEY_BANNED_IPS not in request.app or @@ -108,15 +108,13 @@ async def process_wrong_login(request): new_ban = IpBan(remote_addr) request.app[KEY_BANNED_IPS].append(new_ban) - hass = request.app['hass'] await hass.async_add_job( update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban) _LOGGER.warning( "Banned IP %s for too many login attempts", remote_addr) - persistent_notification.async_create( - hass, + hass.components.persistent_notification.async_create( 'Too many login attempts from {}'.format(remote_addr), 'Banning IP address', NOTIFICATION_ID_BAN) diff --git a/homeassistant/components/huawei_lte.py b/homeassistant/components/huawei_lte.py new file mode 100644 index 00000000000..33da6be56db --- /dev/null +++ b/homeassistant/components/huawei_lte.py @@ -0,0 +1,127 @@ +""" +Support for Huawei LTE routers. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/huawei_lte/ +""" +from datetime import timedelta +from functools import reduce +import logging +import operator + +import voluptuous as vol +import attr + +from homeassistant.const import ( + CONF_URL, CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle + + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['huawei-lte-api==1.0.12'] + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + +DOMAIN = 'huawei_lte' +DATA_KEY = 'huawei_lte' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_URL): cv.url, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + })]) +}, extra=vol.ALLOW_EXTRA) + + +@attr.s +class RouterData: + """Class for router state.""" + + client = attr.ib() + device_information = attr.ib(init=False, factory=dict) + device_signal = attr.ib(init=False, factory=dict) + traffic_statistics = attr.ib(init=False, factory=dict) + wlan_host_list = attr.ib(init=False, factory=dict) + + def __getitem__(self, path: str): + """ + Get value corresponding to a dotted path. + + The first path component designates a member of this class + such as device_information, device_signal etc, and the remaining + path points to a value in the member's data structure. + """ + root, *rest = path.split(".") + try: + data = getattr(self, root) + except AttributeError as err: + raise KeyError from err + return reduce(operator.getitem, rest, data) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self) -> None: + """Call API to update data.""" + self.device_information = self.client.device.information() + _LOGGER.debug("device_information=%s", self.device_information) + self.device_signal = self.client.device.signal() + _LOGGER.debug("device_signal=%s", self.device_signal) + self.traffic_statistics = self.client.monitoring.traffic_statistics() + _LOGGER.debug("traffic_statistics=%s", self.traffic_statistics) + self.wlan_host_list = self.client.wlan.host_list() + _LOGGER.debug("wlan_host_list=%s", self.wlan_host_list) + + +@attr.s +class HuaweiLteData: + """Shared state.""" + + data = attr.ib(init=False, factory=dict) + + def get_data(self, config): + """Get the requested or the only data value.""" + if CONF_URL in config: + return self.data.get(config[CONF_URL]) + if len(self.data) == 1: + return next(iter(self.data.values())) + + return None + + +def setup(hass, config) -> bool: + """Set up Huawei LTE component.""" + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = HuaweiLteData() + for conf in config.get(DOMAIN, []): + _setup_lte(hass, conf) + return True + + +def _setup_lte(hass, lte_config) -> None: + """Set up Huawei LTE router.""" + from huawei_lte_api.AuthorizedConnection import AuthorizedConnection + from huawei_lte_api.Client import Client + + url = lte_config[CONF_URL] + username = lte_config[CONF_USERNAME] + password = lte_config[CONF_PASSWORD] + + connection = AuthorizedConnection( + url, + username=username, + password=password, + ) + client = Client(connection) + + data = RouterData(client) + data.update() + hass.data[DATA_KEY].data[url] = data + + def cleanup(event): + """Clean up resources.""" + client.user.logout() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json index 73613f237da..5414bf01ea7 100644 --- a/homeassistant/components/hue/.translations/fr.json +++ b/homeassistant/components/hue/.translations/fr.json @@ -24,6 +24,6 @@ "title": "Hub de liaison" } }, - "title": "Pont Philips Hue" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/id.json b/homeassistant/components/hue/.translations/id.json new file mode 100644 index 00000000000..bf5557436ce --- /dev/null +++ b/homeassistant/components/hue/.translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Semua Philips Hue bridges sudah dikonfigurasi", + "already_configured": "Bridge sudah dikonfigurasi", + "cannot_connect": "Tidak dapat terhubung ke bridge", + "discover_timeout": "Tidak dapat menemukan Hue Bridges.", + "no_bridges": "Bridge Philips Hue tidak ditemukan", + "unknown": "Kesalahan tidak dikenal terjadi." + }, + "error": { + "linking": "Terjadi kesalahan tautan tidak dikenal.", + "register_failed": "Gagal mendaftar, silakan coba lagi." + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Pilih Hue bridge" + }, + "link": { + "description": "Tekan tombol di bridge untuk mendaftar Philips Hue dengan Home Assistant.\n\n![Lokasi tombol di bridge] (/static/images/config_philips_hue.jpg)", + "title": "Tautan Hub" + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/nn.json b/homeassistant/components/hue/.translations/nn.json new file mode 100644 index 00000000000..45d6bc89d72 --- /dev/null +++ b/homeassistant/components/hue/.translations/nn.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue-bruer er allereie konfiguert", + "already_configured": "Brua er allereie konfiguert", + "cannot_connect": "Klarte ikkje \u00e5 kople til brua", + "discover_timeout": "Klarte ikkje \u00e5 oppdage Hue-bruer", + "no_bridges": "Oppdaga ingen Philips Hue-bruer", + "unknown": "Ukjent feil oppstod" + }, + "error": { + "linking": "Ukjent linkefeil oppstod.", + "register_failed": "Kunne ikkje registrere, pr\u00f8v igjen" + }, + "step": { + "init": { + "data": { + "host": "Vert" + }, + "title": "Vel Hue bru" + }, + "link": { + "description": "Trykk p\u00e5 knappen p\u00e5 brua, for \u00e5 registrere Philips Hue med Home Assistant.\n\n![Lokasjon til knappen p\u00e5 brua]\n(/statisk/bilete/konfiguer_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 38b521078f4..7a781c99f53 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -140,7 +140,7 @@ async def async_setup_entry(hass, entry): config = bridge.api.config device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( - config_entry=entry.entry_id, + config_entry_id=entry.entry_id, connections={ (dr.CONNECTION_NETWORK_MAC, config.mac) }, diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 49ebbdaabf5..24ad65e1feb 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -6,7 +6,7 @@ import os import async_timeout import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -41,10 +41,11 @@ def _find_username_from_config(hass, filename): @config_entries.HANDLERS.register(DOMAIN) -class HueFlowHandler(data_entry_flow.FlowHandler): +class HueFlowHandler(config_entries.ConfigFlow): """Handle a Hue config flow.""" VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the Hue flow.""" diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py deleted file mode 100644 index 9497282ab21..00000000000 --- a/homeassistant/components/ifttt.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Support to trigger Maker IFTTT recipes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ifttt/ -""" -import logging - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['pyfttt==0.3'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_EVENT = 'event' -ATTR_VALUE1 = 'value1' -ATTR_VALUE2 = 'value2' -ATTR_VALUE3 = 'value3' - -CONF_KEY = 'key' - -DOMAIN = 'ifttt' - -SERVICE_TRIGGER = 'trigger' - -SERVICE_TRIGGER_SCHEMA = vol.Schema({ - vol.Required(ATTR_EVENT): cv.string, - vol.Optional(ATTR_VALUE1): cv.string, - vol.Optional(ATTR_VALUE2): cv.string, - vol.Optional(ATTR_VALUE3): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_KEY): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def trigger(hass, event, value1=None, value2=None, value3=None): - """Trigger a Maker IFTTT recipe.""" - data = { - ATTR_EVENT: event, - ATTR_VALUE1: value1, - ATTR_VALUE2: value2, - ATTR_VALUE3: value3, - } - hass.services.call(DOMAIN, SERVICE_TRIGGER, data) - - -def setup(hass, config): - """Set up the IFTTT service component.""" - key = config[DOMAIN][CONF_KEY] - - def trigger_service(call): - """Handle IFTTT trigger service calls.""" - event = call.data[ATTR_EVENT] - value1 = call.data.get(ATTR_VALUE1) - value2 = call.data.get(ATTR_VALUE2) - value3 = call.data.get(ATTR_VALUE3) - - try: - import pyfttt - pyfttt.send_event(key, event, value1, value2, value3) - except requests.exceptions.RequestException: - _LOGGER.exception("Error communicating with IFTTT") - - hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service, - schema=SERVICE_TRIGGER_SCHEMA) - - return True diff --git a/homeassistant/components/ifttt/.translations/en.json b/homeassistant/components/ifttt/.translations/en.json new file mode 100644 index 00000000000..dae4b24de47 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + }, + "step": { + "user": { + "description": "Are you sure you want to set up IFTTT?", + "title": "Set up the IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py new file mode 100644 index 00000000000..534217a7ba2 --- /dev/null +++ b/homeassistant/components/ifttt/__init__.py @@ -0,0 +1,135 @@ +""" +Support to trigger Maker IFTTT recipes. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ifttt/ +""" +from ipaddress import ip_address +import logging +from urllib.parse import urlparse + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries +from homeassistant.util.network import is_local + +REQUIREMENTS = ['pyfttt==0.3'] +DEPENDENCIES = ['webhook'] + +_LOGGER = logging.getLogger(__name__) + +EVENT_RECEIVED = 'ifttt_webhook_received' + +ATTR_EVENT = 'event' +ATTR_VALUE1 = 'value1' +ATTR_VALUE2 = 'value2' +ATTR_VALUE3 = 'value3' + +CONF_KEY = 'key' +CONF_WEBHOOK_ID = 'webhook_id' + +DOMAIN = 'ifttt' + +SERVICE_TRIGGER = 'trigger' + +SERVICE_TRIGGER_SCHEMA = vol.Schema({ + vol.Required(ATTR_EVENT): cv.string, + vol.Optional(ATTR_VALUE1): cv.string, + vol.Optional(ATTR_VALUE2): cv.string, + vol.Optional(ATTR_VALUE3): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ + vol.Required(CONF_KEY): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the IFTTT service component.""" + if DOMAIN not in config: + return True + + key = config[DOMAIN][CONF_KEY] + + def trigger_service(call): + """Handle IFTTT trigger service calls.""" + event = call.data[ATTR_EVENT] + value1 = call.data.get(ATTR_VALUE1) + value2 = call.data.get(ATTR_VALUE2) + value3 = call.data.get(ATTR_VALUE3) + + try: + import pyfttt + pyfttt.send_event(key, event, value1, value2, value3) + except requests.exceptions.RequestException: + _LOGGER.exception("Error communicating with IFTTT") + + hass.services.async_register(DOMAIN, SERVICE_TRIGGER, trigger_service, + schema=SERVICE_TRIGGER_SCHEMA) + + return True + + +async def handle_webhook(hass, webhook_id, data): + """Handle webhook callback.""" + if isinstance(data, dict): + data['webhook_id'] = webhook_id + hass.bus.async_fire(EVENT_RECEIVED, data) + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + entry.data['webhook_id'], handle_webhook) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data['webhook_id']) + return True + + +@config_entries.HANDLERS.register(DOMAIN) +class ConfigFlow(config_entries.ConfigFlow): + """Handle an IFTTT config flow.""" + + async def async_step_user(self, user_input=None): + """Handle a user initiated set up flow.""" + if self._async_current_entries(): + return self.async_abort(reason='one_instance_allowed') + + try: + url_parts = urlparse(self.hass.config.api.base_url) + + if is_local(ip_address(url_parts.hostname)): + return self.async_abort(reason='not_internet_accessible') + except ValueError: + # If it's not an IP address, it's very likely publicly accessible + pass + + if user_input is None: + return self.async_show_form( + step_id='user', + ) + + webhook_id = self.hass.components.webhook.async_generate_id() + webhook_url = \ + self.hass.components.webhook.async_generate_url(webhook_id) + + return self.async_create_entry( + title='IFTTT Webhook', + data={ + CONF_WEBHOOK_ID: webhook_id + }, + description_placeholders={ + 'applet_url': 'https://ifttt.com/maker_webhooks', + 'webhook_url': webhook_url, + 'docs_url': + 'https://www.home-assistant.io/components/ifttt/' + } + ) diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json new file mode 100644 index 00000000000..9fc47504b9b --- /dev/null +++ b/homeassistant/components/ifttt/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "IFTTT", + "step": { + "user": { + "title": "Set up the IFTTT Webhook Applet", + "description": "Are you sure you want to set up IFTTT?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + } + } +} diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 480ec31da7d..84d92361541 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -17,7 +17,6 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -67,20 +66,6 @@ SERVICE_SCAN_SCHEMA = vol.Schema({ }) -@bind_hass -def scan(hass, entity_id=None): - """Force process of all cameras or given entity.""" - hass.add_job(async_scan, hass, entity_id) - - -@callback -@bind_hass -def async_scan(hass, entity_id=None): - """Force process of all cameras or given entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) - - async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index b9c4dcc685e..18c9808c6d2 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -46,24 +46,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id): - """Set input_boolean to True.""" - hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def turn_off(hass, entity_id): - """Set input_boolean to False.""" - hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def toggle(hass, entity_id): - """Set input_boolean to False.""" - hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) - - async def async_setup(hass, config): """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number.py index 2f25ca143b8..2eb811e1fb0 100644 --- a/homeassistant/components/input_number.py +++ b/homeassistant/components/input_number.py @@ -15,7 +15,6 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -82,31 +81,6 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def set_value(hass, entity_id, value): - """Set input_number to value.""" - hass.services.call(DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - }) - - -@bind_hass -def increment(hass, entity_id): - """Increment value of entity.""" - hass.services.call(DOMAIN, SERVICE_INCREMENT, { - ATTR_ENTITY_ID: entity_id - }) - - -@bind_hass -def decrement(hass, entity_id): - """Decrement value of entity.""" - hass.services.call(DOMAIN, SERVICE_DECREMENT, { - ATTR_ENTITY_ID: entity_id - }) - - @asyncio.coroutine def async_setup(hass, config): """Set up an input slider.""" diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index 04e9b04787c..51175efecbd 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -10,7 +10,6 @@ import logging import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -78,40 +77,6 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def select_option(hass, entity_id, option): - """Set value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, { - ATTR_ENTITY_ID: entity_id, - ATTR_OPTION: option, - }) - - -@bind_hass -def select_next(hass, entity_id): - """Set next value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_NEXT, { - ATTR_ENTITY_ID: entity_id, - }) - - -@bind_hass -def select_previous(hass, entity_id): - """Set previous value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_PREVIOUS, { - ATTR_ENTITY_ID: entity_id, - }) - - -@bind_hass -def set_options(hass, entity_id, options): - """Set options of input_select.""" - hass.services.call(DOMAIN, SERVICE_SET_OPTIONS, { - ATTR_ENTITY_ID: entity_id, - ATTR_OPTIONS: options, - }) - - @asyncio.coroutine def async_setup(hass, config): """Set up an input select.""" diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py index 2cb4f58a130..fcc2352f523 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -12,7 +12,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) -from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state @@ -74,15 +73,6 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def set_value(hass, entity_id, value): - """Set input_text to value.""" - hass.services.call(DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - }) - - @asyncio.coroutine def async_setup(hass, config): """Set up an input text box.""" diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index d79640b77ab..749d167e6de 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -7,6 +7,8 @@ https://home-assistant.io/components/insteon/ import asyncio import collections import logging +from typing import Dict + import voluptuous as vol from homeassistant.core import callback @@ -18,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.13.1'] +REQUIREMENTS = ['insteonplm==0.14.2'] _LOGGER = logging.getLogger(__name__) @@ -27,9 +29,9 @@ DOMAIN = 'insteon' CONF_IP_PORT = 'ip_port' CONF_HUB_USERNAME = 'username' CONF_HUB_PASSWORD = 'password' +CONF_HUB_VERSION = 'hub_version' CONF_OVERRIDE = 'device_override' -CONF_PLM_HUB_MSG = ('Must configure either a PLM port or a Hub host, username ' - 'and password') +CONF_PLM_HUB_MSG = 'Must configure either a PLM port or a Hub host' CONF_ADDRESS = 'address' CONF_CAT = 'cat' CONF_SUBCAT = 'subcat' @@ -66,6 +68,22 @@ EVENT_BUTTON_ON = 'insteon.button_on' EVENT_BUTTON_OFF = 'insteon.button_off' EVENT_CONF_BUTTON = 'button' + +def set_default_port(schema: Dict) -> Dict: + """Set the default port based on the Hub version.""" + # If the ip_port is found do nothing + # If it is not found the set the default + ip_port = schema.get(CONF_IP_PORT) + if not ip_port: + hub_version = schema.get(CONF_HUB_VERSION) + # Found hub_version but not ip_port + if hub_version == 1: + schema[CONF_IP_PORT] = 9761 + else: + schema[CONF_IP_PORT] = 25105 + return schema + + CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ vol.Required(CONF_ADDRESS): cv.string, @@ -88,12 +106,13 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All( vol.Schema( {vol.Exclusive(CONF_PORT, 'plm_or_hub', - msg=CONF_PLM_HUB_MSG): cv.isdevice, + msg=CONF_PLM_HUB_MSG): cv.string, vol.Exclusive(CONF_HOST, 'plm_or_hub', msg=CONF_PLM_HUB_MSG): cv.string, - vol.Optional(CONF_IP_PORT, default=25105): int, + vol.Optional(CONF_IP_PORT): cv.port, vol.Optional(CONF_HUB_USERNAME): cv.string, vol.Optional(CONF_HUB_PASSWORD): cv.string, + vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]), vol.Optional(CONF_OVERRIDE): vol.All( cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]), vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), @@ -103,14 +122,7 @@ CONFIG_SCHEMA = vol.Schema({ [CONF_X10_SCHEMA]) }, extra=vol.ALLOW_EXTRA, required=True), cv.has_at_least_one_key(CONF_PORT, CONF_HOST), - vol.Schema( - {vol.Inclusive(CONF_HOST, 'hub', - msg=CONF_PLM_HUB_MSG): cv.string, - vol.Inclusive(CONF_HUB_USERNAME, 'hub', - msg=CONF_PLM_HUB_MSG): cv.string, - vol.Inclusive(CONF_HUB_PASSWORD, 'hub', - msg=CONF_PLM_HUB_MSG): cv.string, - }, extra=vol.ALLOW_EXTRA, required=True)) + set_default_port) }, extra=vol.ALLOW_EXTRA) @@ -151,6 +163,7 @@ def async_setup(hass, config): ip_port = conf.get(CONF_IP_PORT) username = conf.get(CONF_HUB_USERNAME) password = conf.get(CONF_HUB_PASSWORD) + hub_version = conf.get(CONF_HUB_VERSION) overrides = conf.get(CONF_OVERRIDE, []) x10_devices = conf.get(CONF_X10, []) x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF) @@ -284,6 +297,7 @@ def async_setup(hass, config): port=ip_port, username=username, password=password, + hub_version=hub_version, loop=hass.loop, workdir=hass.config.config_dir) else: diff --git a/homeassistant/components/ios/.translations/ca.json b/homeassistant/components/ios/.translations/ca.json new file mode 100644 index 00000000000..1b1ed732ab3 --- /dev/null +++ b/homeassistant/components/ios/.translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Voleu configurar el component Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/cs.json b/homeassistant/components/ios/.translations/cs.json new file mode 100644 index 00000000000..95d675076da --- /dev/null +++ b/homeassistant/components/ios/.translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/de.json b/homeassistant/components/ios/.translations/de.json new file mode 100644 index 00000000000..e9e592d18c2 --- /dev/null +++ b/homeassistant/components/ios/.translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Es wird nur eine Konfiguration von Home Assistant iOS ben\u00f6tigt" + }, + "step": { + "confirm": { + "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/en.json b/homeassistant/components/ios/.translations/en.json new file mode 100644 index 00000000000..ae2e4e03f74 --- /dev/null +++ b/homeassistant/components/ios/.translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of Home Assistant iOS is necessary." + }, + "step": { + "confirm": { + "description": "Do you want to set up the Home Assistant iOS component?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/fr.json b/homeassistant/components/ios/.translations/fr.json new file mode 100644 index 00000000000..934849549e7 --- /dev/null +++ b/homeassistant/components/ios/.translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Seule une configuration de Home Assistant iOS est n\u00e9cessaire." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer le composant Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/he.json b/homeassistant/components/ios/.translations/he.json new file mode 100644 index 00000000000..e786e5ae843 --- /dev/null +++ b/homeassistant/components/ios/.translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Home Assistant iOS \u05e0\u05d7\u05d5\u05e6\u05d4." + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/hu.json b/homeassistant/components/ios/.translations/hu.json new file mode 100644 index 00000000000..5ee001db3c5 --- /dev/null +++ b/homeassistant/components/ios/.translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Csak egyetlen Home Assistant iOS konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Home Assistant iOS komponenst?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/id.json b/homeassistant/components/ios/.translations/id.json new file mode 100644 index 00000000000..5813d9488f0 --- /dev/null +++ b/homeassistant/components/ios/.translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Hanya satu konfigurasi Home Assistant iOS yang diperlukan." + }, + "step": { + "confirm": { + "description": "Apakah Anda ingin mengatur komponen iOS Home Assistant?", + "title": "Home Asisten iOS" + } + }, + "title": "Home Asisten iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/ko.json b/homeassistant/components/ios/.translations/ko.json new file mode 100644 index 00000000000..1496dab0555 --- /dev/null +++ b/homeassistant/components/ios/.translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 Home Assistant iOS \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/lb.json b/homeassistant/components/ios/.translations/lb.json new file mode 100644 index 00000000000..731371cada9 --- /dev/null +++ b/homeassistant/components/ios/.translations/lb.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Home Assistant iOS ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "W\u00ebllt dir d'Home Assistant iOS Komponent ariichten?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/nl.json b/homeassistant/components/ios/.translations/nl.json new file mode 100644 index 00000000000..8e5c46692a0 --- /dev/null +++ b/homeassistant/components/ios/.translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Home Assistant iOS nodig." + }, + "step": { + "confirm": { + "description": "Wilt u het Home Assistant iOS component instellen?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/nn.json b/homeassistant/components/ios/.translations/nn.json new file mode 100644 index 00000000000..9d2cf692006 --- /dev/null +++ b/homeassistant/components/ios/.translations/nn.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Du treng berre \u00e9in Home Assistant iOS-konfigurasjon." + }, + "step": { + "confirm": { + "description": "Vil du sette opp Home Assistant iOS-komponenten?", + "title": "Home Assistant Ios" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/no.json b/homeassistant/components/ios/.translations/no.json new file mode 100644 index 00000000000..a125b96a070 --- /dev/null +++ b/homeassistant/components/ios/.translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Kun en enkelt konfigurasjon av Home Assistant iOS er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 konfigurere Home Assistant iOS-komponenten?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/pl.json b/homeassistant/components/ios/.translations/pl.json new file mode 100644 index 00000000000..6240f074cfc --- /dev/null +++ b/homeassistant/components/ios/.translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/pt-BR.json b/homeassistant/components/ios/.translations/pt-BR.json new file mode 100644 index 00000000000..77efc04b817 --- /dev/null +++ b/homeassistant/components/ios/.translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do Home Assistant iOS \u00e9 necess\u00e1ria." + }, + "step": { + "confirm": { + "description": "Deseja configurar o componente iOS do Home Assistant?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/pt.json b/homeassistant/components/ios/.translations/pt.json new file mode 100644 index 00000000000..6752606d9f5 --- /dev/null +++ b/homeassistant/components/ios/.translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do componente iOS do Home Assistante \u00e9 necess\u00e1ria." + }, + "step": { + "confirm": { + "description": "Deseja configurar o componente iOS do Home Assistant?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/ru.json b/homeassistant/components/ios/.translations/ru.json new file mode 100644 index 00000000000..7030f18b729 --- /dev/null +++ b/homeassistant/components/ios/.translations/ru.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0414\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/sl.json b/homeassistant/components/ios/.translations/sl.json new file mode 100644 index 00000000000..28e9102aafd --- /dev/null +++ b/homeassistant/components/ios/.translations/sl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Potrebna je samo ena konfiguracija Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti komponento za Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/sv.json b/homeassistant/components/ios/.translations/sv.json new file mode 100644 index 00000000000..6806f9bab90 --- /dev/null +++ b/homeassistant/components/ios/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Vill du konfigurera Home Assistants iOS komponent?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/zh-Hans.json b/homeassistant/components/ios/.translations/zh-Hans.json new file mode 100644 index 00000000000..0de30f0f3da --- /dev/null +++ b/homeassistant/components/ios/.translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Home Assistant iOS \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8bbe\u7f6e Home Assistant iOS \u7ec4\u4ef6\uff1f", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/zh-Hant.json b/homeassistant/components/ios/.translations/zh-Hant.json new file mode 100644 index 00000000000..8cfedf31673 --- /dev/null +++ b/homeassistant/components/ios/.translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Home Assistant iOS \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant iOS \u5143\u4ef6\uff1f", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios/__init__.py similarity index 79% rename from homeassistant/components/ios.py rename to homeassistant/components/ios/__init__.py index 7f7377469fd..a67be0a63de 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios/__init__.py @@ -9,15 +9,15 @@ import logging import datetime import voluptuous as vol -# from voluptuous.humanize import humanize_error +from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR, HTTP_BAD_REQUEST) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import ( + config_validation as cv, discovery, config_entry_flow) from homeassistant.util.json import load_json, save_json @@ -164,62 +164,70 @@ IDENTIFY_SCHEMA = vol.Schema({ CONFIGURATION_FILE = '.ios.conf' -CONFIG_FILE = {ATTR_DEVICES: {}} -CONFIG_FILE_PATH = "" - - -def devices_with_push(): +def devices_with_push(hass): """Return a dictionary of push enabled targets.""" targets = {} - for device_name, device in CONFIG_FILE[ATTR_DEVICES].items(): + for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items(): if device.get(ATTR_PUSH_ID) is not None: targets[device_name] = device.get(ATTR_PUSH_ID) return targets -def enabled_push_ids(): +def enabled_push_ids(hass): """Return a list of push enabled target push IDs.""" push_ids = list() - for device in CONFIG_FILE[ATTR_DEVICES].values(): + for device in hass.data[DOMAIN][ATTR_DEVICES].values(): if device.get(ATTR_PUSH_ID) is not None: push_ids.append(device.get(ATTR_PUSH_ID)) return push_ids -def devices(): +def devices(hass): """Return a dictionary of all identified devices.""" - return CONFIG_FILE[ATTR_DEVICES] + return hass.data[DOMAIN][ATTR_DEVICES] -def device_name_for_push_id(push_id): +def device_name_for_push_id(hass, push_id): """Return the device name for the push ID.""" - for device_name, device in CONFIG_FILE[ATTR_DEVICES].items(): + for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items(): if device.get(ATTR_PUSH_ID) is push_id: return device_name return None -def setup(hass, config): +async def async_setup(hass, config): """Set up the iOS component.""" - global CONFIG_FILE - global CONFIG_FILE_PATH + conf = config.get(DOMAIN) - CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE) + ios_config = await hass.async_add_executor_job( + load_json, hass.config.path(CONFIGURATION_FILE)) - CONFIG_FILE = load_json(CONFIG_FILE_PATH) + if ios_config == {}: + ios_config[ATTR_DEVICES] = {} - if CONFIG_FILE == {}: - CONFIG_FILE[ATTR_DEVICES] = {} + ios_config[CONF_PUSH] = (conf or {}).get(CONF_PUSH, {}) + hass.data[DOMAIN] = ios_config + + # No entry support for notify component yet discovery.load_platform(hass, 'notify', DOMAIN, {}, config) - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + if conf is not None: + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) - hass.http.register_view(iOSIdentifyDeviceView) + return True - app_config = config.get(DOMAIN, {}) - hass.http.register_view(iOSPushConfigView(app_config.get(CONF_PUSH, {}))) + +async def async_setup_entry(hass, entry): + """Set up an iOS entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, 'sensor')) + + hass.http.register_view( + iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE))) + hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_PUSH])) return True @@ -247,6 +255,10 @@ class iOSIdentifyDeviceView(HomeAssistantView): url = '/api/ios/identify' name = 'api:ios:identify' + def __init__(self, config_path): + """Initiliaze the view.""" + self._config_path = config_path + @asyncio.coroutine def post(self, request): """Handle the POST request for device identification.""" @@ -255,23 +267,31 @@ class iOSIdentifyDeviceView(HomeAssistantView): except ValueError: return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) + hass = request.app['hass'] + # Commented for now while iOS app is getting frequent updates # try: # data = IDENTIFY_SCHEMA(req_data) # except vol.Invalid as ex: - # return self.json_message(humanize_error(request.json, ex), - # HTTP_BAD_REQUEST) + # return self.json_message( + # vol.humanize.humanize_error(request.json, ex), + # HTTP_BAD_REQUEST) data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat() name = data.get(ATTR_DEVICE_ID) - CONFIG_FILE[ATTR_DEVICES][name] = data + hass.data[DOMAIN][ATTR_DEVICES][name] = data try: - save_json(CONFIG_FILE_PATH, CONFIG_FILE) + save_json(self._config_path, hass.data[DOMAIN]) except HomeAssistantError: return self.json_message("Error saving device.", HTTP_INTERNAL_SERVER_ERROR) return self.json({"status": "registered"}) + + +config_entry_flow.register_discovery_flow( + DOMAIN, 'Home Assistant iOS', lambda *_: True, + config_entries.CONN_CLASS_CLOUD_PUSH) diff --git a/homeassistant/components/ios/strings.json b/homeassistant/components/ios/strings.json new file mode 100644 index 00000000000..cbb63cf8229 --- /dev/null +++ b/homeassistant/components/ios/strings.json @@ -0,0 +1,14 @@ +{ + "config": { + "title": "Home Assistant iOS", + "step": { + "confirm": { + "title": "Home Assistant iOS", + "description": "Do you want to set up the Home Assistant iOS component?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Home Assistant iOS is necessary." + } + } +} diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 3df28586313..388fa41f36f 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -19,7 +19,7 @@ from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN, - ATTR_ENTITY_ID, ATTR_STATE) + ATTR_ENTITY_ID, ATTR_STATE, STATE_ON) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv @@ -35,6 +35,7 @@ CONF_API_HOST = 'api_host' CONF_MOMENTARY = 'momentary' CONF_PAUSE = 'pause' CONF_REPEAT = 'repeat' +CONF_INVERSE = 'inverse' STATE_LOW = 'low' STATE_HIGH = 'high' @@ -48,6 +49,7 @@ _BINARY_SENSOR_SCHEMA = vol.All( vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERSE): cv.boolean, }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) ) @@ -156,6 +158,7 @@ class ConfiguredDevice: CONF_TYPE: entity[CONF_TYPE], CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format( self.device_id[6:], PIN_TO_ZONE[pin])), + CONF_INVERSE: entity.get(CONF_INVERSE), ATTR_STATE: None } _LOGGER.debug('Set up sensor %s (initial state: %s)', @@ -259,15 +262,19 @@ class DiscoveredDevice: def update_initial_states(self): """Update the initial state of each sensor from status poll.""" - for sensor in self.status.get('sensors'): - entity_id = self.stored_configuration[CONF_BINARY_SENSORS]. \ - get(sensor.get(CONF_PIN), {}). \ - get(ATTR_ENTITY_ID) + for sensor_data in self.status.get('sensors'): + sensor_config = self.stored_configuration[CONF_BINARY_SENSORS]. \ + get(sensor_data.get(CONF_PIN), {}) + entity_id = sensor_config.get(ATTR_ENTITY_ID) + + state = bool(sensor_data.get(ATTR_STATE)) + if sensor_config.get(CONF_INVERSE): + state = not state async_dispatcher_send( self.hass, SIGNAL_SENSOR_UPDATE.format(entity_id), - bool(sensor.get(ATTR_STATE))) + state) def sync_device_config(self): """Sync the new pin configuration to the Konnected device.""" @@ -321,6 +328,43 @@ class KonnectedView(HomeAssistantView): """Initialize the view.""" self.auth_token = auth_token + @staticmethod + def binary_value(state, activation): + """Return binary value for GPIO based on state and activation.""" + if activation == STATE_HIGH: + return 1 if state == STATE_ON else 0 + return 0 if state == STATE_ON else 1 + + async def get(self, request: Request, device_id) -> Response: + """Return the current binary state of a switch.""" + hass = request.app['hass'] + pin_num = int(request.query.get('pin')) + data = hass.data[DOMAIN] + + device = data[CONF_DEVICES][device_id] + if not device: + return self.json_message( + 'Device ' + device_id + ' not configured', + status_code=HTTP_NOT_FOUND) + + try: + pin = next(filter( + lambda switch: switch[CONF_PIN] == pin_num, + device[CONF_SWITCHES])) + except StopIteration: + pin = None + + if not pin: + return self.json_message( + 'Switch on pin ' + pin_num + ' not configured', + status_code=HTTP_NOT_FOUND) + + return self.json( + {'pin': pin_num, + 'state': self.binary_value( + hass.states.get(pin[ATTR_ENTITY_ID]).state, + pin[CONF_ACTIVATION])}) + async def put(self, request: Request, device_id, pin_num=None, state=None) -> Response: """Receive a sensor update via PUT request and async set state.""" @@ -341,7 +385,6 @@ class KonnectedView(HomeAssistantView): return self.json_message( "unauthorized", status_code=HTTP_UNAUTHORIZED) pin_num = int(pin_num) - state = bool(int(state)) device = data[CONF_DEVICES].get(device_id) if device is None: return self.json_message('unregistered device', @@ -356,6 +399,9 @@ class KonnectedView(HomeAssistantView): if entity_id is None: return self.json_message('uninitialized sensor/actuator', status_code=HTTP_NOT_FOUND) + state = bool(int(state)) + if pin_data.get(CONF_INVERSE): + state = not state async_dispatcher_send( hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index bc7f136322b..41dbbcd6d0c 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -17,7 +17,6 @@ from homeassistant.components.group import \ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import ToggleEntity @@ -142,90 +141,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None, - color_temp=None, kelvin=None, white_value=None, - profile=None, flash=None, effect=None, color_name=None): - """Turn all or specified light on.""" - hass.add_job( - async_turn_on, hass, entity_id, transition, brightness, brightness_pct, - rgb_color, xy_color, hs_color, color_temp, kelvin, white_value, - profile, flash, effect, color_name) - - -@callback -@bind_hass -def async_turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, - hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, - color_name=None): - """Turn all or specified light on.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_PROFILE, profile), - (ATTR_TRANSITION, transition), - (ATTR_BRIGHTNESS, brightness), - (ATTR_BRIGHTNESS_PCT, brightness_pct), - (ATTR_RGB_COLOR, rgb_color), - (ATTR_XY_COLOR, xy_color), - (ATTR_HS_COLOR, hs_color), - (ATTR_COLOR_TEMP, color_temp), - (ATTR_KELVIN, kelvin), - (ATTR_WHITE_VALUE, white_value), - (ATTR_FLASH, flash), - (ATTR_EFFECT, effect), - (ATTR_COLOR_NAME, color_name), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) - - -@bind_hass -def turn_off(hass, entity_id=None, transition=None): - """Turn all or specified light off.""" - hass.add_job(async_turn_off, hass, entity_id, transition) - - -@callback -@bind_hass -def async_turn_off(hass, entity_id=None, transition=None): - """Turn all or specified light off.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_TRANSITION, transition), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, data)) - - -@callback -@bind_hass -def async_toggle(hass, entity_id=None, transition=None): - """Toggle all or specified light.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_TRANSITION, transition), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TOGGLE, data)) - - -@bind_hass -def toggle(hass, entity_id=None, transition=None): - """Toggle all or specified light.""" - hass.add_job(async_toggle, hass, entity_id, transition) - - def preprocess_turn_on_alternatives(params): """Process extra data for turn light on request.""" profile = Profiles.get(params.pop(ATTR_PROFILE, None)) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index ff3fe609924..d3bec079a4c 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -6,7 +6,8 @@ https://home-assistant.io/components/light.deconz/ """ from homeassistant.components.deconz.const import ( CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, - DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN, SWITCH_TYPES) + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN, + COVER_TYPES, SWITCH_TYPES) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, @@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Add light from deCONZ.""" entities = [] for light in lights: - if light.type not in SWITCH_TYPES: + if light.type not in COVER_TYPES + SWITCH_TYPES: entities.append(DeconzLight(light)) async_add_entities(entities, True) @@ -213,6 +214,7 @@ class DeconzLight(Light): self._light.uniqueid.count(':') != 7): return None serial = self._light.uniqueid.split('-', 1)[0] + bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid return { 'connections': {(CONNECTION_ZIGBEE, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)}, @@ -220,4 +222,5 @@ class DeconzLight(Light): 'model': self._light.modelid, 'name': self._light.name, 'sw_version': self._light.swversion, + 'via_hub': (DECONZ_DOMAIN, bridgeid), } diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 6f6e0ed617e..958abaca033 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -302,6 +302,7 @@ class HueLight(Light): 'model': self.light.productname or self.light.modelid, # Not yet exposed as properties in aiohue 'sw_version': self.light.raw['swversion'], + 'via_hub': (hue.DOMAIN, self.bridge.api.config.bridgeid), } async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 06507eaeca6..d54aa3cd4ce 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -31,12 +31,14 @@ class ISYLightDevice(ISYDevice, Light): @property def is_on(self) -> bool: """Get whether the ISY994 light is on.""" - return self.value > 0 + if self.is_unknown(): + return False + return self.value != 0 @property def brightness(self) -> float: """Get the brightness of the ISY994 light.""" - return self.value + return None if self.is_unknown() else self.value def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index bea39354e1b..21a501fa6ea 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -30,7 +30,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.1.2'] +REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.0'] UDP_BROADCAST_PORT = 56700 diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 225f0f510ad..3b095aa4bfd 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, @@ -19,10 +19,13 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.helpers.restore_state import async_get_last_state +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -54,6 +57,7 @@ CONF_WHITE_VALUE_SCALE = 'white_value_scale' CONF_WHITE_VALUE_STATE_TOPIC = 'white_value_state_topic' CONF_WHITE_VALUE_TEMPLATE = 'white_value_template' CONF_ON_COMMAND_TYPE = 'on_command_type' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_NAME = 'MQTT Light' @@ -79,6 +83,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, @@ -100,17 +105,34 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up a MQTT Light.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT light through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT light dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT light.""" + 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(light.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up a MQTT Light.""" config.setdefault( CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE)) async_add_entities([MqttLight( config.get(CONF_NAME), + config.get(CONF_UNIQUE_ID), config.get(CONF_EFFECT_LIST), { key: config.get(key) for key in ( @@ -153,20 +175,23 @@ async def async_setup_platform(hass, config, async_add_entities, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttLight(MqttAvailability, Light): +class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): """Representation of a MQTT light.""" - def __init__(self, name, effect_list, topic, templates, qos, - retain, payload, optimistic, brightness_scale, + def __init__(self, name, unique_id, effect_list, topic, templates, + qos, retain, payload, optimistic, brightness_scale, white_value_scale, on_command_type, availability_topic, - payload_available, payload_not_available): + payload_available, payload_not_available, discovery_hash): """Initialize MQTT light.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name + self._unique_id = unique_id self._effect_list = effect_list self._topic = topic self._qos = qos @@ -212,10 +237,12 @@ class MqttLight(MqttAvailability, Light): SUPPORT_WHITE_VALUE) self._supported_features |= ( topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) templates = {} for key, tpl in list(self._templates.items()): @@ -231,6 +258,10 @@ class MqttLight(MqttAvailability, Light): def state_received(topic, payload, qos): """Handle new MQTT messages.""" payload = templates[CONF_STATE](payload) + if not payload: + _LOGGER.debug("Ignoring empty state message from '%s'", topic) + return + if payload == self._payload['on']: self._state = True elif payload == self._payload['off']: @@ -247,7 +278,13 @@ class MqttLight(MqttAvailability, Light): @callback def brightness_received(topic, payload, qos): """Handle new MQTT messages for the brightness.""" - device_value = float(templates[CONF_BRIGHTNESS](payload)) + payload = templates[CONF_BRIGHTNESS](payload) + if not payload: + _LOGGER.debug("Ignoring empty brightness message from '%s'", + topic) + return + + device_value = float(payload) percent_bright = device_value / self._brightness_scale self._brightness = int(percent_bright * 255) self.async_schedule_update_ha_state() @@ -268,8 +305,12 @@ class MqttLight(MqttAvailability, Light): @callback def rgb_received(topic, payload, qos): """Handle new MQTT messages for RGB.""" - rgb = [int(val) for val in - templates[CONF_RGB](payload).split(',')] + payload = templates[CONF_RGB](payload) + if not payload: + _LOGGER.debug("Ignoring empty rgb message from '%s'", topic) + return + + rgb = [int(val) for val in payload.split(',')] self._hs = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() @@ -287,7 +328,13 @@ class MqttLight(MqttAvailability, Light): @callback def color_temp_received(topic, payload, qos): """Handle new MQTT messages for color temperature.""" - self._color_temp = int(templates[CONF_COLOR_TEMP](payload)) + payload = templates[CONF_COLOR_TEMP](payload) + if not payload: + _LOGGER.debug("Ignoring empty color temp message from '%s'", + topic) + return + + self._color_temp = int(payload) self.async_schedule_update_ha_state() if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: @@ -306,7 +353,12 @@ class MqttLight(MqttAvailability, Light): @callback def effect_received(topic, payload, qos): """Handle new MQTT messages for effect.""" - self._effect = templates[CONF_EFFECT](payload) + payload = templates[CONF_EFFECT](payload) + if not payload: + _LOGGER.debug("Ignoring empty effect message from '%s'", topic) + return + + self._effect = payload self.async_schedule_update_ha_state() if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: @@ -325,7 +377,13 @@ class MqttLight(MqttAvailability, Light): @callback def white_value_received(topic, payload, qos): """Handle new MQTT messages for white value.""" - device_value = float(templates[CONF_WHITE_VALUE](payload)) + payload = templates[CONF_WHITE_VALUE](payload) + if not payload: + _LOGGER.debug("Ignoring empty white value message from '%s'", + topic) + return + + device_value = float(payload) percent_white = device_value / self._white_value_scale self._white_value = int(percent_white * 255) self.async_schedule_update_ha_state() @@ -346,8 +404,13 @@ class MqttLight(MqttAvailability, Light): @callback def xy_received(topic, payload, qos): """Handle new MQTT messages for color.""" - xy_color = [float(val) for val in - templates[CONF_XY](payload).split(',')] + payload = templates[CONF_XY](payload) + if not payload: + _LOGGER.debug("Ignoring empty xy-color message from '%s'", + topic) + return + + xy_color = [float(val) for val in payload.split(',')] self._hs = color_util.color_xy_to_hs(*xy_color) self.async_schedule_update_ha_state() @@ -392,6 +455,11 @@ class MqttLight(MqttAvailability, Light): """Return the name of the device if any.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 239c924ed2b..1ed43a6385a 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -4,29 +4,31 @@ Support for MQTT JSON lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_json/ """ -import logging import json +import logging +from typing import Optional + import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_HS_COLOR, - FLASH_LONG, FLASH_SHORT, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, - SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, FLASH_LONG, FLASH_SHORT, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, + Light) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE -from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, STATE_ON, - CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.const import ( + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, + CONF_RGB, CONF_WHITE_VALUE, CONF_XY, STATE_ON) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.restore_state import async_get_last_state +from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -53,6 +55,7 @@ CONF_EFFECT_LIST = 'effect_list' CONF_FLASH_TIME_LONG = 'flash_time_long' CONF_FLASH_TIME_SHORT = 'flash_time_short' CONF_HS = 'hs' +CONF_UNIQUE_ID = 'unique_id' # Stealing some of these from the base MQTT configs. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -67,6 +70,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), @@ -85,8 +89,14 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, """Set up a MQTT JSON Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) + + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttJson( config.get(CONF_NAME), + config.get(CONF_UNIQUE_ID), config.get(CONF_EFFECT_LIST), { key: config.get(key) for key in ( @@ -113,21 +123,25 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), - config.get(CONF_BRIGHTNESS_SCALE) + config.get(CONF_BRIGHTNESS_SCALE), + discovery_hash, )]) -class MqttJson(MqttAvailability, Light): +class MqttJson(MqttAvailability, MqttDiscoveryUpdate, Light): """Representation of a MQTT JSON light.""" - def __init__(self, name, effect_list, topic, qos, retain, optimistic, - brightness, color_temp, effect, rgb, white_value, xy, hs, - flash_times, availability_topic, payload_available, - payload_not_available, brightness_scale): + def __init__(self, name, unique_id, effect_list, topic, qos, retain, + optimistic, brightness, color_temp, effect, rgb, white_value, + xy, hs, flash_times, availability_topic, payload_available, + payload_not_available, brightness_scale, + discovery_hash: Optional[str]): """Initialize MQTT JSON light.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name + self._unique_id = unique_id self._effect_list = effect_list self._topic = topic self._qos = qos @@ -176,7 +190,8 @@ class MqttJson(MqttAvailability, Light): async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) last_state = await async_get_last_state(self.hass, self.entity_id) @@ -316,6 +331,11 @@ class MqttJson(MqttAvailability, Light): """Return the name of the device if any.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/light/opple.py b/homeassistant/components/light/opple.py new file mode 100644 index 00000000000..66850d04406 --- /dev/null +++ b/homeassistant/components/light/opple.py @@ -0,0 +1,147 @@ +""" +Support for the Opple light. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.opple/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, Light) +from homeassistant.const import CONF_HOST, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.util.color import \ + color_temperature_kelvin_to_mired as kelvin_to_mired +from homeassistant.util.color import \ + color_temperature_mired_to_kelvin as mired_to_kelvin + +REQUIREMENTS = ['pyoppleio==1.0.5'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "opple light" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Opple light platform.""" + name = config[CONF_NAME] + host = config[CONF_HOST] + entity = OppleLight(name, host) + add_entities([entity]) + + _LOGGER.debug("Init light %s %s", host, entity.unique_id) + + +class OppleLight(Light): + """Opple light device.""" + + def __init__(self, name, host): + """Initialize an Opple light.""" + from pyoppleio.OppleLightDevice import OppleLightDevice + self._device = OppleLightDevice(host) + + self._name = name + self._is_on = None + self._brightness = None + self._color_temp = None + + @property + def available(self): + """Return True if light is available.""" + return self._device.is_online + + @property + def unique_id(self): + """Return unique ID for light.""" + return self._device.mac + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def is_on(self): + """Return true if light is on.""" + return self._is_on + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def color_temp(self): + """Return the color temperature of this light.""" + return kelvin_to_mired(self._color_temp) + + @property + def min_mireds(self): + """Return minimum supported color temperature.""" + return 175 + + @property + def max_mireds(self): + """Return maximum supported color temperature.""" + return 333 + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + _LOGGER.debug("Turn on light %s %s", self._device.ip, kwargs) + if not self.is_on: + self._device.power_on = True + + if ATTR_BRIGHTNESS in kwargs and \ + self.brightness != kwargs[ATTR_BRIGHTNESS]: + self._device.brightness = kwargs[ATTR_BRIGHTNESS] + + if ATTR_COLOR_TEMP in kwargs and \ + self.brightness != kwargs[ATTR_COLOR_TEMP]: + color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + self._device.color_temperature = color_temp + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._device.power_on = False + _LOGGER.debug("Turn off light %s", self._device.ip) + + def update(self): + """Synchronize state with light.""" + prev_available = self.available + self._device.update() + + if prev_available == self.available and \ + self._is_on == self._device.power_on and \ + self._brightness == self._device.brightness and \ + self._color_temp == self._device.color_temperature: + return + + if not self.available: + _LOGGER.debug("Light %s is offline", self._device.ip) + return + + self._is_on = self._device.power_on + self._brightness = self._device.brightness + self._color_temp = self._device.color_temperature + + if not self.is_on: + _LOGGER.debug("Update light %s success: power off", + self._device.ip) + else: + _LOGGER.debug("Update light %s success: power on brightness %s " + "color temperature %s", + self._device.ip, self._brightness, self._color_temp) diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index 5a0e0546b1f..25c72c247ee 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -REQUIREMENTS = ['pwmled==1.2.1'] +REQUIREMENTS = ['pwmled==1.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index a1e46c07d7d..0cc02e82b65 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -19,7 +19,7 @@ from homeassistant.util.color import \ from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) -REQUIREMENTS = ['pyHS100==0.3.2'] +REQUIREMENTS = ['pyHS100==0.3.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 0d12d095bb6..b62900b204c 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -13,8 +13,10 @@ from homeassistant.components.light import ( SUPPORT_COLOR, Light) from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA -from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ - KEY_API +from homeassistant.components.tradfri import ( + KEY_GATEWAY, KEY_API, DOMAIN as TRADFRI_DOMAIN) +from homeassistant.components.tradfri.const import ( + CONF_IMPORT_GROUPS, CONF_GATEWAY_ID) import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -31,28 +33,21 @@ SUPPORTED_FEATURES = SUPPORT_TRANSITION SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION -async def async_setup_platform(hass, config, - async_add_entities, discovery_info=None): - """Set up the IKEA Tradfri Light platform.""" - if discovery_info is None: - return +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Tradfri lights based on a config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] + api = hass.data[KEY_API][config_entry.entry_id] + gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] - gateway_id = discovery_info['gateway'] - api = hass.data[KEY_API][gateway_id] - gateway = hass.data[KEY_GATEWAY][gateway_id] - - devices_command = gateway.get_devices() - devices_commands = await api(devices_command) + devices_commands = await api(gateway.get_devices()) devices = await api(devices_commands) lights = [dev for dev in devices if dev.has_light_control] if lights: async_add_entities( TradfriLight(light, api, gateway_id) for light in lights) - allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] - if allow_tradfri_groups: - groups_command = gateway.get_groups() - groups_commands = await api(groups_command) + if config_entry.data[CONF_IMPORT_GROUPS]: + groups_commands = await api(gateway.get_groups()) groups = await api(groups_commands) if groups: async_add_entities( @@ -167,6 +162,7 @@ class TradfriLight(Light): self._hs_color = None self._features = SUPPORTED_FEATURES self._available = True + self._gateway_id = gateway_id self._refresh(light) @@ -175,6 +171,22 @@ class TradfriLight(Light): """Return unique ID for light.""" return self._unique_id + @property + def device_info(self): + """Return the device info.""" + info = self._light.device_info + + return { + 'identifiers': { + (TRADFRI_DOMAIN, self._light.id) + }, + 'name': self._name, + 'manufacturer': info.manufacturer, + 'model': info.model_number, + 'sw_version': info.firmware_version, + 'via_hub': (TRADFRI_DOMAIN, self._gateway_id), + } + @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index a08ebe459b4..b14b1f96e69 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -45,7 +45,7 @@ DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, - vol.Optional(CONF_SAVE_ON_CHANGE, default=True): cv.boolean, + vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index dc5c4977944..56a1e9e5169 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -81,40 +81,65 @@ class Light(zha.Entity, light.Light): async def async_turn_on(self, **kwargs): """Turn the entity on.""" + from zigpy.exceptions import DeliveryError + duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) duration = duration * 10 # tenths of s if light.ATTR_COLOR_TEMP in kwargs: temperature = kwargs[light.ATTR_COLOR_TEMP] - await self._endpoint.light_color.move_to_color_temp( - temperature, duration) + try: + res = await self._endpoint.light_color.move_to_color_temp( + temperature, duration) + _LOGGER.debug("%s: moved to %i color temp: %s", + self.entity_id, temperature, res) + except DeliveryError as ex: + _LOGGER.error("%s: Couldn't change color temp: %s", + self.entity_id, ex) + return self._color_temp = temperature if light.ATTR_HS_COLOR in kwargs: self._hs_color = kwargs[light.ATTR_HS_COLOR] xy_color = color_util.color_hs_to_xy(*self._hs_color) - await self._endpoint.light_color.move_to_color( - int(xy_color[0] * 65535), - int(xy_color[1] * 65535), - duration, - ) + try: + res = await self._endpoint.light_color.move_to_color( + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), + duration, + ) + _LOGGER.debug("%s: moved XY color to (%1.2f, %1.2f): %s", + self.entity_id, xy_color[0], xy_color[1], res) + except DeliveryError as ex: + _LOGGER.error("%s: Couldn't change color temp: %s", + self.entity_id, ex) + return if self._brightness is not None: brightness = kwargs.get( light.ATTR_BRIGHTNESS, self._brightness or 255) self._brightness = brightness # Move to level with on/off: - await self._endpoint.level.move_to_level_with_on_off( - brightness, - duration - ) + try: + res = await self._endpoint.level.move_to_level_with_on_off( + brightness, + duration + ) + _LOGGER.debug("%s: moved to %i level with on/off: %s", + self.entity_id, brightness, res) + except DeliveryError as ex: + _LOGGER.error("%s: Couldn't change brightness level: %s", + self.entity_id, ex) + return self._state = 1 self.async_schedule_update_ha_state() return - from zigpy.exceptions import DeliveryError + try: - await self._endpoint.on_off.on() + res = await self._endpoint.on_off.on() + _LOGGER.debug("%s was turned on: %s", self.entity_id, res) except DeliveryError as ex: - _LOGGER.error("Unable to turn the light on: %s", ex) + _LOGGER.error("%s: Unable to turn the light on: %s", + self.entity_id, ex) return self._state = 1 @@ -124,9 +149,11 @@ class Light(zha.Entity, light.Light): """Turn the entity off.""" from zigpy.exceptions import DeliveryError try: - await self._endpoint.on_off.off() + res = await self._endpoint.on_off.off() + _LOGGER.debug("%s was turned off: %s", self.entity_id, res) except DeliveryError as ex: - _LOGGER.error("Unable to turn the light off: %s", ex) + _LOGGER.error("%s: Unable to turn the light off: %s", + self.entity_id, ex) return self._state = 0 @@ -154,23 +181,31 @@ class Light(zha.Entity, light.Light): async def async_update(self): """Retrieve latest state.""" - result = await zha.safe_read(self._endpoint.on_off, ['on_off']) + result = await zha.safe_read(self._endpoint.on_off, ['on_off'], + allow_cache=False, + only_cache=(not self._initialized)) self._state = result.get('on_off', self._state) if self._supported_features & light.SUPPORT_BRIGHTNESS: result = await zha.safe_read(self._endpoint.level, - ['current_level']) + ['current_level'], + allow_cache=False, + only_cache=(not self._initialized)) self._brightness = result.get('current_level', self._brightness) if self._supported_features & light.SUPPORT_COLOR_TEMP: result = await zha.safe_read(self._endpoint.light_color, - ['color_temperature']) + ['color_temperature'], + allow_cache=False, + only_cache=(not self._initialized)) self._color_temp = result.get('color_temperature', self._color_temp) if self._supported_features & light.SUPPORT_COLOR: result = await zha.safe_read(self._endpoint.light_color, - ['current_x', 'current_y']) + ['current_x', 'current_y'], + allow_cache=False, + only_cache=(not self._initialized)) if 'current_x' in result and 'current_y' in result: xy_color = (round(result['current_x']/65535, 3), round(result['current_y']/65535, 3)) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 55feef496f8..1e768eb127a 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -63,6 +63,16 @@ def brightness_state(value): return 0, STATE_OFF +def byte_to_zwave_brightness(value): + """Convert brightness in 0-255 scale to 0-99 scale. + + `value` -- (int) Brightness byte value from 0-255. + """ + if value > 0: + return max(1, int((value / 255) * 99)) + return 0 + + def ct_to_hs(temp): """Convert color temperature (mireds) to hs.""" colorlist = list( @@ -187,7 +197,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): # brightness. Level 255 means to set it to previous value. if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - brightness = int((self._brightness / 255) * 99) + brightness = byte_to_zwave_brightness(self._brightness) else: brightness = 255 diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 3c4ff7cdedd..5218ea49c80 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -57,42 +57,6 @@ def is_locked(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_LOCKED) -@bind_hass -def lock(hass, entity_id=None, code=None): - """Lock all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_LOCK, data) - - -@bind_hass -def unlock(hass, entity_id=None, code=None): - """Unlock all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_UNLOCK, data) - - -@bind_hass -def open_lock(hass, entity_id=None, code=None): - """Open all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_OPEN, data) - - @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for locks.""" diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index 103864a6bfd..b9b094b615b 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.lock import LockDevice from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) from homeassistant.components import mqtt @@ -52,6 +52,10 @@ def async_setup_platform(hass, config, async_add_entities, if value_template is not None: value_template.hass = hass + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttLock( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -64,19 +68,22 @@ def async_setup_platform(hass, config, async_add_entities, value_template, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE) + config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttLock(MqttAvailability, LockDevice): +class MqttLock(MqttAvailability, MqttDiscoveryUpdate, LockDevice): """Representation of a lock that can be toggled using MQTT.""" def __init__(self, name, state_topic, command_topic, qos, retain, payload_lock, payload_unlock, optimistic, value_template, - availability_topic, payload_available, payload_not_available): + availability_topic, payload_available, payload_not_available, + discovery_hash): """Initialize the lock.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._state = False self._name = name self._state_topic = state_topic @@ -87,11 +94,13 @@ class MqttLock(MqttAvailability, LockDevice): self._payload_unlock = payload_unlock self._optimistic = optimistic self._template = value_template + self._discovery_hash = discovery_hash @asyncio.coroutine def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + yield from MqttAvailability.async_added_to_hass(self) + yield from MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): diff --git a/homeassistant/components/logi_circle.py b/homeassistant/components/logi_circle.py new file mode 100644 index 00000000000..c0a7f4c2621 --- /dev/null +++ b/homeassistant/components/logi_circle.py @@ -0,0 +1,80 @@ +""" +Support for Logi Circle cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/logi_circle/ +""" +import logging +import asyncio + +import voluptuous as vol +import async_timeout + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + +REQUIREMENTS = ['logi_circle==0.1.7'] + +_LOGGER = logging.getLogger(__name__) +_TIMEOUT = 15 # seconds + +CONF_ATTRIBUTION = "Data provided by circle.logi.com" + +NOTIFICATION_ID = 'logi_notification' +NOTIFICATION_TITLE = 'Logi Circle Setup' + +DOMAIN = 'logi_circle' +DEFAULT_CACHEDB = '.logi_cache.pickle' +DEFAULT_ENTITY_NAMESPACE = 'logi_circle' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Logi Circle component.""" + conf = config[DOMAIN] + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + + try: + from logi_circle import Logi + from logi_circle.exception import BadLogin + from aiohttp.client_exceptions import ClientResponseError + + cache = hass.config.path(DEFAULT_CACHEDB) + logi = Logi(username=username, password=password, cache_file=cache) + + with async_timeout.timeout(_TIMEOUT, loop=hass.loop): + await logi.login() + hass.data[DOMAIN] = await logi.cameras + + if not logi.is_connected: + return False + except (BadLogin, ClientResponseError) as ex: + _LOGGER.error('Unable to connect to Logi Circle API: %s', str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + except asyncio.TimeoutError: + # The TimeoutError exception object returns nothing when casted to a + # string, so we'll handle it separately. + err = '{}s timeout exceeded when connecting to Logi Circle API'.format( + _TIMEOUT) + _LOGGER.error(err) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(err), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + return True diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py new file mode 100644 index 00000000000..eba69159048 --- /dev/null +++ b/homeassistant/components/lovelace/__init__.py @@ -0,0 +1,51 @@ +"""Lovelace UI.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.util.yaml import load_yaml +from homeassistant.exceptions import HomeAssistantError + +DOMAIN = 'lovelace' + +OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' +WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' +SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, + OLD_WS_TYPE_GET_LOVELACE_UI), +}) + + +async def async_setup(hass, config): + """Set up the Lovelace commands.""" + # Backwards compat. Added in 0.80. Remove after 0.85 + hass.components.websocket_api.async_register_command( + OLD_WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) + + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) + + return True + + +@websocket_api.async_response +async def websocket_lovelace_config(hass, connection, msg): + """Send lovelace UI config over websocket config.""" + error = None + try: + config = await hass.async_add_executor_job( + load_yaml, hass.config.path('ui-lovelace.yaml')) + message = websocket_api.result_message( + msg['id'], config + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except HomeAssistantError as err: + error = 'load_error', str(err) + + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message_outside(message) diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 6a648e4dc47..2ed12b23164 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -4,53 +4,53 @@ Provides functionality for mailboxes. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mailbox/ """ - import asyncio -import logging from contextlib import suppress from datetime import timedelta - -import async_timeout +import logging from aiohttp import web from aiohttp.web_exceptions import HTTPNotFound +import async_timeout -from homeassistant.core import callback -from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity from homeassistant.components.http import HomeAssistantView +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, discovery +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_prepare_setup_platform +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['http'] DOMAIN = 'mailbox' + EVENT = 'mailbox_updated' CONTENT_TYPE_MPEG = 'audio/mpeg' +CONTENT_TYPE_NONE = 'none' + SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for mailboxes.""" mailboxes = [] - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'mailbox', 'mailbox', 'mdi:mailbox') hass.http.register_view(MailboxPlatformsView(mailboxes)) hass.http.register_view(MailboxMessageView(mailboxes)) hass.http.register_view(MailboxMediaView(mailboxes)) hass.http.register_view(MailboxDeleteView(mailboxes)) - @asyncio.coroutine - def async_setup_platform(p_type, p_config=None, discovery_info=None): + async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a mailbox platform.""" if p_config is None: p_config = {} if discovery_info is None: discovery_info = {} - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, p_type) if platform is None: @@ -61,10 +61,10 @@ def async_setup(hass, config): mailbox = None try: if hasattr(platform, 'async_get_handler'): - mailbox = yield from \ + mailbox = await \ platform.async_get_handler(hass, p_config, discovery_info) elif hasattr(platform, 'get_handler'): - mailbox = yield from hass.async_add_job( + mailbox = await hass.async_add_executor_job( platform.get_handler, hass, p_config, discovery_info) else: raise HomeAssistantError("Invalid mailbox platform.") @@ -79,21 +79,20 @@ def async_setup(hass, config): return mailboxes.append(mailbox) - mailbox_entity = MailboxEntity(hass, mailbox) + mailbox_entity = MailboxEntity(mailbox) component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - yield from component.async_add_entities([mailbox_entity]) + await component.async_add_entities([mailbox_entity]) setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config in config_per_platform(config, DOMAIN)] if setup_tasks: - yield from asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks, loop=hass.loop) - @asyncio.coroutine - def async_platform_discovered(platform, info): + async def async_platform_discovered(platform, info): """Handle for discovered platform.""" - yield from async_setup_platform(platform, discovery_info=info) + await async_setup_platform(platform, discovery_info=info) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) @@ -101,19 +100,21 @@ def async_setup(hass, config): class MailboxEntity(Entity): - """Entity for each mailbox platform.""" + """Entity for each mailbox platform to provide a badge display.""" - def __init__(self, hass, mailbox): + def __init__(self, mailbox): """Initialize mailbox entity.""" self.mailbox = mailbox - self.hass = hass self.message_count = 0 + async def async_added_to_hass(self): + """Complete entity initialization.""" @callback def _mailbox_updated(event): self.async_schedule_update_ha_state(True) - hass.bus.async_listen(EVENT, _mailbox_updated) + self.hass.bus.async_listen(EVENT, _mailbox_updated) + self.async_schedule_update_ha_state(True) @property def state(self): @@ -125,10 +126,9 @@ class MailboxEntity(Entity): """Return the name of the entity.""" return self.mailbox.name - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve messages from platform.""" - messages = yield from self.mailbox.async_get_messages() + messages = await self.mailbox.async_get_messages() self.message_count = len(messages) @@ -149,13 +149,21 @@ class Mailbox: """Return the supported media type.""" raise NotImplementedError() - @asyncio.coroutine - def async_get_media(self, msgid): + @property + def can_delete(self): + """Return if messages can be deleted.""" + return False + + @property + def has_media(self): + """Return if messages have attached media files.""" + return False + + async def async_get_media(self, msgid): """Return the media blob for the msgid.""" raise NotImplementedError() - @asyncio.coroutine - def async_get_messages(self): + async def async_get_messages(self): """Return a list of the current messages.""" raise NotImplementedError() @@ -191,12 +199,16 @@ class MailboxPlatformsView(MailboxView): url = "/api/mailbox/platforms" name = "api:mailbox:platforms" - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Retrieve list of platforms.""" platforms = [] for mailbox in self.mailboxes: - platforms.append(mailbox.name) + platforms.append( + { + 'name': mailbox.name, + 'has_media': mailbox.has_media, + 'can_delete': mailbox.can_delete + }) return self.json(platforms) @@ -206,11 +218,10 @@ class MailboxMessageView(MailboxView): url = "/api/mailbox/messages/{platform}" name = "api:mailbox:messages" - @asyncio.coroutine - def get(self, request, platform): + async def get(self, request, platform): """Retrieve messages.""" mailbox = self.get_mailbox(platform) - messages = yield from mailbox.async_get_messages() + messages = await mailbox.async_get_messages() return self.json(messages) @@ -220,8 +231,7 @@ class MailboxDeleteView(MailboxView): url = "/api/mailbox/delete/{platform}/{msgid}" name = "api:mailbox:delete" - @asyncio.coroutine - def delete(self, request, platform, msgid): + async def delete(self, request, platform, msgid): """Delete items.""" mailbox = self.get_mailbox(platform) mailbox.async_delete(msgid) @@ -233,8 +243,7 @@ class MailboxMediaView(MailboxView): url = r"/api/mailbox/media/{platform}/{msgid}" name = "api:asteriskmbox:media" - @asyncio.coroutine - def get(self, request, platform, msgid): + async def get(self, request, platform, msgid): """Retrieve media.""" mailbox = self.get_mailbox(platform) @@ -242,7 +251,7 @@ class MailboxMediaView(MailboxView): with suppress(asyncio.CancelledError, asyncio.TimeoutError): with async_timeout.timeout(10, loop=hass.loop): try: - stream = yield from mailbox.async_get_media(msgid) + stream = await mailbox.async_get_media(msgid) except StreamError as err: error_msg = "Error getting media: %s" % (err) _LOGGER.error(error_msg) diff --git a/homeassistant/components/mailbox/asterisk_cdr.py b/homeassistant/components/mailbox/asterisk_cdr.py new file mode 100644 index 00000000000..ae0939c3da5 --- /dev/null +++ b/homeassistant/components/mailbox/asterisk_cdr.py @@ -0,0 +1,64 @@ +""" +Asterisk CDR interface. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/mailbox.asterisk_cdr/ +""" +import logging +import hashlib +import datetime + +from homeassistant.core import callback +from homeassistant.components.asterisk_mbox import SIGNAL_CDR_UPDATE +from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN +from homeassistant.components.mailbox import Mailbox +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['asterisk_mbox'] +_LOGGER = logging.getLogger(__name__) +MAILBOX_NAME = "asterisk_cdr" + + +async def async_get_handler(hass, config, discovery_info=None): + """Set up the Asterix CDR platform.""" + return AsteriskCDR(hass, MAILBOX_NAME) + + +class AsteriskCDR(Mailbox): + """Asterisk VM Call Data Record mailbox.""" + + def __init__(self, hass, name): + """Initialize Asterisk CDR.""" + super().__init__(hass, name) + self.cdr = [] + async_dispatcher_connect( + self.hass, SIGNAL_CDR_UPDATE, self._update_callback) + + @callback + def _update_callback(self, msg): + """Update the message count in HA, if needed.""" + self._build_message() + self.async_update() + + def _build_message(self): + """Build message structure.""" + cdr = [] + for entry in self.hass.data[ASTERISK_DOMAIN].cdr: + timestamp = datetime.datetime.strptime( + entry['time'], "%Y-%m-%d %H:%M:%S").timestamp() + info = { + 'origtime': timestamp, + 'callerid': entry['callerid'], + 'duration': entry['duration'], + } + sha = hashlib.sha256(str(entry).encode('utf-8')).hexdigest() + msg = "Destination: {}\nApplication: {}\n Context: {}".format( + entry['dest'], entry['application'], entry['context']) + cdr.append({'info': info, 'sha': sha, 'text': msg}) + self.cdr = cdr + + async def async_get_messages(self): + """Return a list of the current messages.""" + if not self.cdr: + self._build_message() + return self.cdr diff --git a/homeassistant/components/mailbox/asterisk_mbox.py b/homeassistant/components/mailbox/asterisk_mbox.py index 47d59234d7d..087018084f2 100644 --- a/homeassistant/components/mailbox/asterisk_mbox.py +++ b/homeassistant/components/mailbox/asterisk_mbox.py @@ -4,26 +4,25 @@ Asterisk Voicemail interface. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/mailbox.asteriskvm/ """ -import asyncio import logging +from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN +from homeassistant.components.mailbox import ( + CONTENT_TYPE_MPEG, Mailbox, StreamError) from homeassistant.core import callback -from homeassistant.components.asterisk_mbox import DOMAIN -from homeassistant.components.mailbox import (Mailbox, CONTENT_TYPE_MPEG, - StreamError) from homeassistant.helpers.dispatcher import async_dispatcher_connect -DEPENDENCIES = ['asterisk_mbox'] _LOGGER = logging.getLogger(__name__) -SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' +DEPENDENCIES = ['asterisk_mbox'] + SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request' +SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated' -@asyncio.coroutine -def async_get_handler(hass, config, async_add_entities, discovery_info=None): +async def async_get_handler(hass, config, discovery_info=None): """Set up the Asterix VM platform.""" - return AsteriskMailbox(hass, DOMAIN) + return AsteriskMailbox(hass, ASTERISK_DOMAIN) class AsteriskMailbox(Mailbox): @@ -45,24 +44,32 @@ class AsteriskMailbox(Mailbox): """Return the supported media type.""" return CONTENT_TYPE_MPEG - @asyncio.coroutine - def async_get_media(self, msgid): + @property + def can_delete(self): + """Return if messages can be deleted.""" + return True + + @property + def has_media(self): + """Return if messages have attached media files.""" + return True + + async def async_get_media(self, msgid): """Return the media blob for the msgid.""" from asterisk_mbox import ServerError - client = self.hass.data[DOMAIN].client + client = self.hass.data[ASTERISK_DOMAIN].client try: return client.mp3(msgid, sync=True) except ServerError as err: raise StreamError(err) - @asyncio.coroutine - def async_get_messages(self): + async def async_get_messages(self): """Return a list of the current messages.""" - return self.hass.data[DOMAIN].messages + return self.hass.data[ASTERISK_DOMAIN].messages def async_delete(self, msgid): """Delete the specified messages.""" - client = self.hass.data[DOMAIN].client + client = self.hass.data[ASTERISK_DOMAIN].client _LOGGER.info("Deleting: %s", msgid) client.delete(msgid) return True diff --git a/homeassistant/components/mailbox/demo.py b/homeassistant/components/mailbox/demo.py index 8096a4fabb7..2aabde42b36 100644 --- a/homeassistant/components/mailbox/demo.py +++ b/homeassistant/components/mailbox/demo.py @@ -4,24 +4,22 @@ Asterisk Voicemail interface. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/mailbox.asteriskvm/ """ -import asyncio +from hashlib import sha1 import logging import os -from hashlib import sha1 +from homeassistant.components.mailbox import ( + CONTENT_TYPE_MPEG, Mailbox, StreamError) from homeassistant.util import dt -from homeassistant.components.mailbox import (Mailbox, CONTENT_TYPE_MPEG, - StreamError) - _LOGGER = logging.getLogger(__name__) -DOMAIN = "DemoMailbox" + +MAILBOX_NAME = "DemoMailbox" -@asyncio.coroutine -def async_get_handler(hass, config, discovery_info=None): +async def async_get_handler(hass, config, discovery_info=None): """Set up the Demo mailbox.""" - return DemoMailbox(hass, DOMAIN) + return DemoMailbox(hass, MAILBOX_NAME) class DemoMailbox(Mailbox): @@ -38,11 +36,15 @@ class DemoMailbox(Mailbox): msgtxt = "Message {}. {}".format( idx + 1, txt * (1 + idx * (idx % 2))) msgsha = sha1(msgtxt.encode('utf-8')).hexdigest() - msg = {"info": {"origtime": msgtime, - "callerid": "John Doe <212-555-1212>", - "duration": "10"}, - "text": msgtxt, - "sha": msgsha} + msg = { + 'info': { + 'origtime': msgtime, + 'callerid': 'John Doe <212-555-1212>', + 'duration': '10', + }, + 'text': msgtxt, + 'sha': msgsha, + } self._messages[msgsha] = msg @property @@ -50,8 +52,17 @@ class DemoMailbox(Mailbox): """Return the supported media type.""" return CONTENT_TYPE_MPEG - @asyncio.coroutine - def async_get_media(self, msgid): + @property + def can_delete(self): + """Return if messages can be deleted.""" + return True + + @property + def has_media(self): + """Return if messages have attached media files.""" + return True + + async def async_get_media(self, msgid): """Return the media blob for the msgid.""" if msgid not in self._messages: raise StreamError("Message not found") @@ -61,8 +72,7 @@ class DemoMailbox(Mailbox): with open(audio_path, 'rb') as file: return file.read() - @asyncio.coroutine - def async_get_messages(self): + async def async_get_messages(self): """Return a list of the current messages.""" return sorted(self._messages.values(), key=lambda item: item['info']['origtime'], diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 8f2abb9be19..a7093579805 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.08.22'] +REQUIREMENTS = ['youtube_dl==2018.09.18'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 7c49b095c66..235ca8d5b2d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -15,33 +15,32 @@ from random import SystemRandom from urllib.parse import urlparse from aiohttp import web -from aiohttp.hdrs import CONTENT_TYPE, CACHE_CONTROL +from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE import async_timeout import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.const import ( - STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID, - SERVICE_TOGGLE, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, - SERVICE_MEDIA_PLAY, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, - SERVICE_VOLUME_SET, SERVICE_MEDIA_PAUSE, SERVICE_SHUFFLE_SET, - SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_MEDIA_NEXT_TRACK, - SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK) + ATTR_ENTITY_ID, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, SERVICE_SHUFFLE_SET, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, + STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) +from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass -from homeassistant.components import websocket_api _LOGGER = logging.getLogger(__name__) _RND = SystemRandom() DOMAIN = 'media_player' DEPENDENCIES = ['http'] -SCAN_INTERVAL = timedelta(seconds=10) ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -97,6 +96,8 @@ MEDIA_TYPE_CHANNEL = 'channel' MEDIA_TYPE_PLAYLIST = 'playlist' MEDIA_TYPE_URL = 'url' +SCAN_INTERVAL = timedelta(seconds=10) + SUPPORT_PAUSE = 1 SUPPORT_SEEK = 2 SUPPORT_VOLUME_SET = 4 @@ -191,168 +192,6 @@ def is_on(hass, entity_id=None): for entity_id in entity_ids) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on specified media player 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 media player 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 media player or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def volume_up(hass, entity_id=None): - """Send the media player the command for volume up.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) - - -@bind_hass -def volume_down(hass, entity_id=None): - """Send the media player the command for volume down.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) - - -@bind_hass -def mute_volume(hass, mute, entity_id=None): - """Send the media player the command for muting the volume.""" - data = {ATTR_MEDIA_VOLUME_MUTED: mute} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data) - - -@bind_hass -def set_volume_level(hass, volume, entity_id=None): - """Send the media player the command for setting the volume.""" - data = {ATTR_MEDIA_VOLUME_LEVEL: volume} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data) - - -@bind_hass -def media_play_pause(hass, entity_id=None): - """Send the media player the command for play/pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) - - -@bind_hass -def media_play(hass, entity_id=None): - """Send the media player the command for play/pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) - - -@bind_hass -def media_pause(hass, entity_id=None): - """Send the media player the command for pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) - - -@bind_hass -def media_stop(hass, entity_id=None): - """Send the media player the stop command.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data) - - -@bind_hass -def media_next_track(hass, entity_id=None): - """Send the media player the command for next track.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) - - -@bind_hass -def media_previous_track(hass, entity_id=None): - """Send the media player the command for prev track.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) - - -@bind_hass -def media_seek(hass, position, entity_id=None): - """Send the media player the command to seek in current playing media.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_MEDIA_SEEK_POSITION] = position - hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data) - - -@bind_hass -def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): - """Send the media player the command for playing media.""" - data = {ATTR_MEDIA_CONTENT_TYPE: media_type, - ATTR_MEDIA_CONTENT_ID: media_id} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - if enqueue: - data[ATTR_MEDIA_ENQUEUE] = enqueue - - hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) - - -@bind_hass -def select_source(hass, source, entity_id=None): - """Send the media player the command to select input source.""" - data = {ATTR_INPUT_SOURCE: source} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data) - - -@bind_hass -def select_sound_mode(hass, sound_mode, entity_id=None): - """Send the media player the command to select sound mode.""" - data = {ATTR_SOUND_MODE: sound_mode} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SELECT_SOUND_MODE, data) - - -@bind_hass -def clear_playlist(hass, entity_id=None): - """Send the media player the command for clear playlist.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data) - - -@bind_hass -def set_shuffle(hass, shuffle, entity_id=None): - """Send the media player the command to enable/disable shuffle mode.""" - data = {ATTR_MEDIA_SHUFFLE: shuffle} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SHUFFLE_SET, data) - - WS_TYPE_MEDIA_PLAYER_THUMBNAIL = 'media_player_thumbnail' SCHEMA_WEBSOCKET_GET_THUMBNAIL = \ websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index 359ee0a9254..33b6e28a890 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -4,17 +4,17 @@ Support for Anthem Network Receivers and Processors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.anthemav/ """ -import logging import asyncio +import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE, + PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, STATE_UNKNOWN, - EVENT_HOMEASSISTANT_STOP) + CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, + STATE_ON, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['anthemav==1.1.8'] @@ -32,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }) +}) @asyncio.coroutine diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py index 360ccd0f522..399e59ae9f5 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/media_player/apple_tv.py @@ -7,20 +7,19 @@ https://home-assistant.io/components/media_player.apple_tv/ import asyncio import logging -from homeassistant.core import callback from homeassistant.components.apple_tv import ( ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES) from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_STOP, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, MediaPlayerDevice, MEDIA_TYPE_MUSIC, - MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW) + MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + MediaPlayerDevice) from homeassistant.const import ( - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST, - STATE_OFF, CONF_NAME, EVENT_HOMEASSISTANT_STOP) + CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, + STATE_PAUSED, STATE_PLAYING, STATE_STANDBY) +from homeassistant.core import callback import homeassistant.util.dt as dt_util - DEPENDENCIES = ['apple_tv'] _LOGGER = logging.getLogger(__name__) @@ -31,8 +30,8 @@ SUPPORT_APPLE_TV = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Apple TV platform.""" if not discovery_info: return diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py index 6e8cc727121..ac399307126 100644 --- a/homeassistant/components/media_player/aquostv.py +++ b/homeassistant/components/media_player/aquostv.py @@ -9,16 +9,13 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_SELECT_SOURCE, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, - SUPPORT_VOLUME_SET, MediaPlayerDevice, PLATFORM_SCHEMA) - + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, - CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT) - - + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT, + CONF_USERNAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['sharp_aquos_rc==0.3.2'] diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py index 7869093138c..2c78bb24bbd 100644 --- a/homeassistant/components/media_player/blackbird.py +++ b/homeassistant/components/media_player/blackbird.py @@ -13,15 +13,15 @@ from homeassistant.components.media_player import ( DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON) + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_OFF, + STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyblackbird==0.5'] _LOGGER = logging.getLogger(__name__) -SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE +SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE ZONE_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, @@ -33,7 +33,6 @@ SOURCE_SCHEMA = vol.Schema({ CONF_ZONES = 'zones' CONF_SOURCES = 'sources' -CONF_TYPE = 'type' DATA_BLACKBIRD = 'blackbird' diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index 1fe939b34ef..ab012402636 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -24,8 +24,8 @@ from homeassistant.components.media_player import ( MediaPlayerDevice) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, - STATE_OFF, STATE_PAUSED, STATE_PLAYING) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, + STATE_PAUSED, STATE_PLAYING) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index 9f4496582ad..04dc013108f 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -10,11 +10,11 @@ import re import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, - SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, - PLATFORM_SCHEMA) -from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 2954e427ed5..67d8ea0b419 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -9,24 +9,24 @@ import logging import threading from typing import Optional, Tuple -import voluptuous as vol import attr +import voluptuous as vol -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType, ConfigType -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import (dispatcher_send, - async_dispatcher_connect) from homeassistant.components.cast import DOMAIN as CAST_DOMAIN from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, - EVENT_HOMEASSISTANT_STOP) + CONF_HOST, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, + STATE_PLAYING) +from homeassistant.core import callback +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util DEPENDENCIES = ('cast',) @@ -57,8 +57,8 @@ SIGNAL_CAST_DISCOVERED = 'cast_discovered' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list, - [cv.string]) + vol.Optional(CONF_IGNORE_CEC, default=[]): + vol.All(cv.ensure_list, [cv.string]), }) @@ -212,9 +212,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not isinstance(config, list): config = [config] - await asyncio.wait([ + # no pending task + done, _ = await asyncio.wait([ _async_setup_platform(hass, cfg, async_add_entities, None) for cfg in config]) + if any([task.exception() for task in done]): + raise PlatformNotReady async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -246,8 +249,8 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, if cast_device is not None: async_add_entities([cast_device]) - async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, - async_cast_discovered) + remove_handler = async_dispatcher_connect( + hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) # Re-play the callback for all past chromecasts, store the objects in # a list to avoid concurrent modification resulting in exception. for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): @@ -261,8 +264,11 @@ async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, info = await hass.async_add_job(_fill_out_missing_chromecast_info, info) if info.friendly_name is None: - # HTTP dial failed, so we won't be able to connect. + _LOGGER.debug("Cannot retrieve detail information for chromecast" + " %s, the device may not be online", info) + remove_handler() raise PlatformNotReady + hass.async_add_job(_discover_chromecast, hass, info) @@ -364,12 +370,11 @@ class CastDevice(MediaPlayerDevice): if self._chromecast is not None: if old_cast_info.host_port == cast_info.host_port: - # Nothing connection-related updated + _LOGGER.debug("No connection related update: %s", + cast_info.host_port) return await self._async_disconnect() - # Failed connection will unfortunately never raise an exception, it - # will instead just try connecting indefinitely. # pylint: disable=protected-access _LOGGER.debug("Connecting to cast device %s", cast_info) chromecast = await self.hass.async_add_job( @@ -400,7 +405,12 @@ class CastDevice(MediaPlayerDevice): await self.hass.async_add_job(self._chromecast.disconnect) - # Invalidate some attributes + self._invalidate() + + self.async_schedule_update_ha_state() + + def _invalidate(self): + """Invalidate some attributes.""" self._chromecast = None self.cast_status = None self.media_status = None @@ -409,8 +419,6 @@ class CastDevice(MediaPlayerDevice): self._status_listener.invalidate() self._status_listener = None - self.async_schedule_update_ha_state() - # ========== Callbacks ========== def new_cast_status(self, cast_status): """Handle updates of the cast status.""" @@ -425,7 +433,16 @@ class CastDevice(MediaPlayerDevice): def new_connection_status(self, connection_status): """Handle updates of connection status.""" - from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED + from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED + + _LOGGER.debug("Received cast device connection status: %s", + connection_status.status) + if connection_status.status == CONNECTION_STATUS_DISCONNECTED: + self._available = False + self._invalidate() + self.schedule_update_ha_state() + return new_available = connection_status.status == CONNECTION_STATUS_CONNECTED if new_available != self._available: diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index fcfa16b33ac..43259c40f65 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -9,18 +9,18 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA, + DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, + MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, MediaPlayerDevice) - from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, - ATTR_ENTITY_ID) - + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_PAUSED, + STATE_PLAYING) import homeassistant.helpers.config_validation as cv +REQUIREMENTS = ['pychannels==1.0.0'] + _LOGGER = logging.getLogger(__name__) DATA_CHANNELS = 'channels' @@ -52,16 +52,11 @@ CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({ vol.Required(ATTR_SECONDS): vol.Coerce(int), }) -REQUIREMENTS = ['pychannels==1.0.0'] - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Channels platform.""" device = ChannelsPlayer( - config.get('name'), - config.get(CONF_HOST), - config.get(CONF_PORT) - ) + config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT)) if DATA_CHANNELS not in hass.data: hass.data[DATA_CHANNELS] = [] @@ -77,8 +72,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device.entity_id == entity_id), None) if device is None: - _LOGGER.warning("Unable to find Channels with entity_id: %s", - entity_id) + _LOGGER.warning( + "Unable to find Channels with entity_id: %s", entity_id) return if service.service == SERVICE_SEEK_FORWARD: @@ -90,12 +85,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device.seek_by(seconds) hass.services.register( - DOMAIN, SERVICE_SEEK_FORWARD, service_handler, - schema=CHANNELS_SCHEMA) + DOMAIN, SERVICE_SEEK_FORWARD, service_handler, schema=CHANNELS_SCHEMA) hass.services.register( - DOMAIN, SERVICE_SEEK_BACKWARD, service_handler, - schema=CHANNELS_SCHEMA) + DOMAIN, SERVICE_SEEK_BACKWARD, service_handler, schema=CHANNELS_SCHEMA) hass.services.register( DOMAIN, SERVICE_SEEK_BY, service_handler, diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py index fab2bef73f0..e38c44b8d27 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -4,7 +4,6 @@ Support for Clementine Music Player as media player. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.clementine/ """ - import asyncio from datetime import timedelta import logging @@ -12,24 +11,24 @@ import time import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA, - SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MEDIA_TYPE_MUSIC, - SUPPORT_VOLUME_SET, MediaPlayerDevice) + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, CONF_ACCESS_TOKEN, - STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) + CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, + STATE_PAUSED, STATE_PLAYING) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['python-clementine-remote==1.0.1'] -SCAN_INTERVAL = timedelta(seconds=5) - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Clementine Remote' DEFAULT_PORT = 5500 +SCAN_INTERVAL = timedelta(seconds=5) + SUPPORT_CLEMENTINE = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_VOLUME_SET | \ SUPPORT_NEXT_TRACK | \ @@ -69,7 +68,7 @@ class ClementineDevice(MediaPlayerDevice): self._track_name = '' self._track_artist = '' self._track_album_name = '' - self._state = STATE_UNKNOWN + self._state = None def update(self): """Retrieve the latest data from the Clementine Player.""" diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index 6f579fd9791..2711ac1ff11 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -8,15 +8,14 @@ import logging import voluptuous as vol - from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PLAY, - SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK, PLATFORM_SCHEMA, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, - CONF_PASSWORD) + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED, + STATE_PLAYING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pycmus==0.1.1'] diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 06f2a3f1155..c2a736f531e 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -5,11 +5,12 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE, SUPPORT_CLEAR_PLAYLIST, - SUPPORT_PLAY, SUPPORT_SHUFFLE_SET, MediaPlayerDevice) + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, + SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOUND_MODE, + SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + MediaPlayerDevice) from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING import homeassistant.util.dt as dt_util @@ -20,8 +21,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): DemoYoutubePlayer( 'Living Room', 'eyU3bRy2x44', '♥♥ The Best Fireplace Video (3 hours)', 300), - DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours', - 360000), + DemoYoutubePlayer( + 'Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours', 360000), DemoMusicPlayer(), DemoTVShowPlayer(), ]) diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 320211e700f..c0f296c2fb8 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -10,10 +10,10 @@ import telnetlib import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE, - SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice) + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 14839590bee..296548dd3c2 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -5,35 +5,37 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.denon/ """ -import logging from collections import namedtuple +import logging + import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE, - SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice, - PLATFORM_SCHEMA, SUPPORT_TURN_ON, MEDIA_TYPE_MUSIC, - SUPPORT_VOLUME_SET, SUPPORT_PLAY) + MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED, - CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) + CONF_HOST, CONF_NAME, CONF_TIMEOUT, CONF_ZONE, STATE_OFF, STATE_ON, + STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['denonavr==0.7.5'] _LOGGER = logging.getLogger(__name__) +ATTR_SOUND_MODE_RAW = 'sound_mode_raw' + +CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)' +CONF_SHOW_ALL_SOURCES = 'show_all_sources' +CONF_VALID_ZONES = ['Zone2', 'Zone3'] +CONF_ZONES = 'zones' + DEFAULT_SHOW_SOURCES = False DEFAULT_TIMEOUT = 2 -CONF_SHOW_ALL_SOURCES = 'show_all_sources' -CONF_ZONES = 'zones' -CONF_VALID_ZONES = ['Zone2', 'Zone3'] -CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)' -KEY_DENON_CACHE = 'denonavr_hosts' -ATTR_SOUND_MODE_RAW = 'sound_mode_raw' +KEY_DENON_CACHE = 'denonavr_hosts' SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index e03474cdb38..42293ba25fe 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -4,29 +4,28 @@ Support for the DirecTV receivers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.directv/ """ -import voluptuous as vol import requests +import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, - MediaPlayerDevice) + MEDIA_TYPE_MOVIE, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT) + CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PLAYING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['directpy==0.5'] DEFAULT_DEVICE = '0' -DEFAULT_NAME = 'DirecTV Receiver' +DEFAULT_NAME = "DirecTV Receiver" DEFAULT_PORT = 8080 SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY -DATA_DIRECTV = "data_directv" +DATA_DIRECTV = 'data_directv' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -51,9 +50,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): elif discovery_info: host = discovery_info.get('host') - name = 'DirecTV_' + discovery_info.get('serial', '') + name = 'DirecTV_{}'.format(discovery_info.get('serial', '')) - # attempt to discover additional RVU units + # Attempt to discover additional RVU units try: resp = requests.get( 'http://%s:%d/info/getLocations' % (host, DEFAULT_PORT)).json() @@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): DEFAULT_PORT, loc["clientAddr"]]) except requests.exceptions.RequestException: - # bail out and just go forward with uPnP data + # Bail out and just go forward with uPnP data if DEFAULT_DEVICE not in known_devices: hosts.append([name, host, DEFAULT_PORT, DEFAULT_DEVICE]) @@ -78,8 +77,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dtvs) hass.data[DATA_DIRECTV] = known_devices - return True - class DirecTvDevice(MediaPlayerDevice): """Representation of a DirecTV receiver on the network.""" diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 6c970ec197e..8aab4bfa43a 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -1,43 +1,36 @@ -# -*- coding: utf-8 -*- """ Support for DLNA DMR (Device Media Renderer). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.dlna_dmr/ """ - import asyncio +from datetime import datetime import functools import logging -from datetime import datetime import aiohttp import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, - MediaPlayerDevice, - PLATFORM_SCHEMA) + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, - CONF_URL, CONF_NAME, - STATE_OFF, STATE_ON, STATE_IDLE, STATE_PLAYING, STATE_PAUSED) + CONF_NAME, CONF_URL, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, + STATE_ON, STATE_PAUSED, STATE_PLAYING) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip +REQUIREMENTS = ['async-upnp-client==0.12.4'] + +_LOGGER = logging.getLogger(__name__) DLNA_DMR_DATA = 'dlna_dmr' -REQUIREMENTS = [ - 'async-upnp-client==0.12.4', -] - DEFAULT_NAME = 'DLNA Digital Media Renderer' DEFAULT_LISTEN_PORT = 8301 @@ -68,8 +61,6 @@ HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = { 'playlist': 'playlist/*', } -_LOGGER = logging.getLogger(__name__) - def catch_request_errors(): """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" @@ -96,13 +87,11 @@ async def async_start_event_handler(hass, server_host, server_port, requester): # start event handler from async_upnp_client.aiohttp import AiohttpNotifyServer - server = AiohttpNotifyServer(requester, - server_port, - server_host, - hass.loop) + server = AiohttpNotifyServer( + requester, server_port, server_host, hass.loop) await server.start_server() - _LOGGER.info('UPNP/DLNA event handler listening on: %s', - server.callback_url) + _LOGGER.info( + 'UPNP/DLNA event handler listening on: %s', server.callback_url) hass_data['notify_server'] = server hass_data['event_handler'] = server.event_handler @@ -116,10 +105,8 @@ async def async_start_event_handler(hass, server_host, server_port, requester): return hass_data['event_handler'] -async def async_setup_platform(hass: HomeAssistant, - config, - async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, config, async_add_entities, discovery_info=None): """Set up DLNA DMR platform.""" if config.get(CONF_URL) is not None: url = config[CONF_URL] @@ -145,10 +132,8 @@ async def async_setup_platform(hass: HomeAssistant, if server_host is None: server_host = get_local_ip() server_port = config.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT) - event_handler = await async_start_event_handler(hass, - server_host, - server_port, - requester) + event_handler = await async_start_event_handler( + hass, server_host, server_port, requester) # create upnp device from async_upnp_client import UpnpFactory @@ -183,10 +168,10 @@ class DlnaDmrDevice(MediaPlayerDevice): """Handle addition.""" self._device.on_event = self._on_event - # register unsubscribe on stop + # Register unsubscribe on stop bus = self.hass.bus - bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, - self._async_on_hass_stop) + bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_on_hass_stop) @property def available(self): @@ -306,23 +291,21 @@ class DlnaDmrDevice(MediaPlayerDevice): mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING[media_type] upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING[media_type] - # stop current playing media + # Stop current playing media if self._device.can_stop: await self.async_media_stop() - # queue media - await self._device.async_set_transport_uri(media_id, - title, - mime_type, - upnp_class) + # +ueue media + await self._device.async_set_transport_uri( + media_id, title, mime_type, upnp_class) await self._device.async_wait_for_can_play() - # if already playing, no need to call Play + # If already playing, no need to call Play from async_upnp_client import dlna if self._device.state == dlna.STATE_PLAYING: return - # play it + # Play it await self.async_media_play() @catch_request_errors() diff --git a/homeassistant/components/media_player/dunehd.py b/homeassistant/components/media_player/dunehd.py index f582ceefe5f..00c8ff3f4df 100644 --- a/homeassistant/components/media_player/dunehd.py +++ b/homeassistant/components/media_player/dunehd.py @@ -6,13 +6,13 @@ https://home-assistant.io/components/media_player.dunehd/ """ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_NEXT_TRACK, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, - SUPPORT_PLAY, MediaPlayerDevice) + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, STATE_ON, STATE_PLAYING) + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pdunehd==1.3'] diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 809db228d02..2bf3a1b803f 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -10,13 +10,13 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, - MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA) + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, SUPPORT_STOP, MediaPlayerDevice) from homeassistant.const import ( - STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, - CONF_HOST, CONF_PORT, CONF_SSL, CONF_API_KEY, DEVICE_DEFAULT_NAME, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, + STATE_PAUSED, STATE_PLAYING) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -42,11 +42,11 @@ SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, }) @@ -95,7 +95,7 @@ def async_setup_platform(hass, config, async_add_entities, if new_devices: _LOGGER.debug("Adding new devices: %s", new_devices) - async_add_entities(new_devices, update_before_add=True) + async_add_entities(new_devices, True) @callback def device_removal_callback(data): diff --git a/homeassistant/components/media_player/epson.py b/homeassistant/components/media_player/epson.py index 23bbf685004..46beb4487fd 100644 --- a/homeassistant/components/media_player/epson.py +++ b/homeassistant/components/media_player/epson.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/media_player.epson/ """ import logging + import voluptuous as vol from homeassistant.components.media_player import ( @@ -20,37 +21,45 @@ import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['epson-projector==0.1.3'] +_LOGGER = logging.getLogger(__name__) + +ATTR_CMODE = 'cmode' + DATA_EPSON = 'epson' DEFAULT_NAME = 'EPSON Projector' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=80): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean -}) - SERVICE_SELECT_CMODE = 'epson_select_cmode' -ATTR_CMODE = 'cmode' SUPPORT_CMODE = 33001 SUPPORT_EPSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE |\ SUPPORT_CMODE | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK -_LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Epson media player platform.""" + from epson_projector.const import (CMODE_LIST_SET) + if DATA_EPSON not in hass.data: hass.data[DATA_EPSON] = [] + name = config.get(CONF_NAME) host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + ssl = config.get(CONF_SSL) + + epson = EpsonProjector(async_get_clientsession( + hass, verify_ssl=False), name, host, port, ssl) - epson = EpsonProjector(async_get_clientsession(hass, verify_ssl=False), - name, host, - config.get(CONF_PORT), config.get(CONF_SSL)) hass.data[DATA_EPSON].append(epson) async_add_entities([epson], update_before_add=True) @@ -67,7 +76,7 @@ async def async_setup_platform(hass, config, async_add_entities, cmode = service.data.get(ATTR_CMODE) await device.select_cmode(cmode) await device.update() - from epson_projector.const import (CMODE_LIST_SET) + epson_schema = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET)) }) @@ -81,13 +90,12 @@ class EpsonProjector(MediaPlayerDevice): def __init__(self, websession, name, host, port, encryption): """Initialize entity to control Epson projector.""" - self._name = name import epson_projector as epson from epson_projector.const import DEFAULT_SOURCES + + self._name = name self._projector = epson.Projector( - host, - websession=websession, - port=port) + host, websession=websession, port=port) self._cmode = None self._source_list = list(DEFAULT_SOURCES.values()) self._source = None @@ -97,9 +105,8 @@ class EpsonProjector(MediaPlayerDevice): async def update(self): """Update state of device.""" from epson_projector.const import ( - EPSON_CODES, POWER, - CMODE, CMODE_LIST, SOURCE, VOLUME, - BUSY, SOURCE_LIST) + EPSON_CODES, POWER, CMODE, CMODE_LIST, SOURCE, VOLUME, BUSY, + SOURCE_LIST) is_turned_on = await self._projector.get_property(POWER) _LOGGER.debug("Project turn on/off status: %s", is_turned_on) if is_turned_on and is_turned_on == EPSON_CODES[POWER]: diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 0594b603a0c..3914d2381b2 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -10,13 +10,13 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA, - SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice) + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( + CONF_DEVICE, CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, - STATE_UNKNOWN, CONF_HOST, CONF_PORT, CONF_SSL, CONF_NAME, CONF_DEVICE, - CONF_DEVICES) + STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py index 6dc4e73b1c0..aebdb676859 100644 --- a/homeassistant/components/media_player/frontier_silicon.py +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -10,14 +10,14 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, - MEDIA_TYPE_MUSIC) + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) from homeassistant.const import ( - STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN, - CONF_HOST, CONF_PORT, CONF_PASSWORD) + CONF_HOST, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_PAUSED, + STATE_PLAYING, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['afsapi==0.0.4'] @@ -42,16 +42,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Frontier Silicon platform.""" import requests if discovery_info is not None: async_add_entities( - [AFSAPIDevice(discovery_info['ssdp_description'], - DEFAULT_PASSWORD)], - update_before_add=True) + [AFSAPIDevice( + discovery_info['ssdp_description'], DEFAULT_PASSWORD)], True) return True host = config.get(CONF_HOST) @@ -60,8 +59,7 @@ def async_setup_platform(hass, config, async_add_entities, try: async_add_entities( - [AFSAPIDevice(DEVICE_URL.format(host, port), password)], - update_before_add=True) + [AFSAPIDevice(DEVICE_URL.format(host, port), password)], True) _LOGGER.debug("FSAPI device %s:%s -> %s", host, port, password) return True except requests.exceptions.RequestException: @@ -78,7 +76,7 @@ class AFSAPIDevice(MediaPlayerDevice): """Initialize the Frontier Silicon API device.""" self._device_url = device_url self._password = password - self._state = STATE_UNKNOWN + self._state = None self._name = None self._title = None diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index 200191ad77a..b16eb8d417a 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -4,19 +4,19 @@ Support for Google Play Music Desktop Player. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.gpmdp/ """ -import logging import json +import logging import socket import time import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_SEEK, SUPPORT_PLAY, - MediaPlayerDevice, PLATFORM_SCHEMA) + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_VOLUME_SET, + MediaPlayerDevice) from homeassistant.const import ( - STATE_PLAYING, STATE_PAUSED, STATE_OFF, CONF_HOST, CONF_PORT, CONF_NAME) + CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json @@ -55,9 +55,11 @@ def request_configuration(hass, config, url, add_entities_callback): return from websocket import create_connection websocket = create_connection((url), timeout=1) - websocket.send(json.dumps({'namespace': 'connect', - 'method': 'connect', - 'arguments': ['Home Assistant']})) + websocket.send(json.dumps({ + 'namespace': 'connect', + 'method': 'connect', + 'arguments': ['Home Assistant'] + })) def gpmdp_configuration_callback(callback_data): """Handle configuration changes.""" diff --git a/homeassistant/components/media_player/gstreamer.py b/homeassistant/components/media_player/gstreamer.py index e2477f0a4cd..e520fcb1033 100644 --- a/homeassistant/components/media_player/gstreamer.py +++ b/homeassistant/components/media_player/gstreamer.py @@ -9,21 +9,18 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PAUSE, - SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, SUPPORT_NEXT_TRACK, - PLATFORM_SCHEMA, MediaPlayerDevice) -from homeassistant.const import ( - STATE_IDLE, CONF_NAME, EVENT_HOMEASSISTANT_STOP) + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_SET, MediaPlayerDevice) +from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE import homeassistant.helpers.config_validation as cv +REQUIREMENTS = ['gstreamer-player==1.1.0'] _LOGGER = logging.getLogger(__name__) - -REQUIREMENTS = ['gstreamer-player==1.1.0'] -DOMAIN = 'gstreamer' CONF_PIPELINE = 'pipeline' +DOMAIN = 'gstreamer' SUPPORT_GSTREAMER = SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_PAUSE |\ SUPPORT_PLAY_MEDIA | SUPPORT_NEXT_TRACK diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py index 9198e4dec88..cb4afadd058 100644 --- a/homeassistant/components/media_player/hdmi_cec.py +++ b/homeassistant/components/media_player/hdmi_cec.py @@ -7,12 +7,12 @@ https://home-assistant.io/components/hdmi_cec/ import logging from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice -from homeassistant.components.media_player import MediaPlayerDevice, DOMAIN, \ - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, \ - SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_STOP, \ - SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE -from homeassistant.const import STATE_ON, STATE_OFF, STATE_PLAYING, \ - STATE_IDLE, STATE_PAUSED +from homeassistant.components.media_player import ( + DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, MediaPlayerDevice) +from homeassistant.const import ( + STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) from homeassistant.core import HomeAssistant DEPENDENCIES = ['hdmi_cec'] diff --git a/homeassistant/components/media_player/horizon.py b/homeassistant/components/media_player/horizon.py index 4fa97cb5537..04471c69b9c 100644 --- a/homeassistant/components/media_player/horizon.py +++ b/homeassistant/components/media_player/horizon.py @@ -4,27 +4,26 @@ Support for the Unitymedia Horizon HD Recorder. For more details about this platform, please refer to the documentation https://home-assistant.io/components/media_player.horizon/ """ - from datetime import timedelta import logging import voluptuous as vol +from homeassistant import util from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_CHANNEL, - SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PAUSE, - SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK) -from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, - STATE_PAUSED, STATE_PLAYING) + MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant import util REQUIREMENTS = ['einder==0.3.1'] _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Horizon" +DEFAULT_NAME = 'Horizon' DEFAULT_PORT = 5900 MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 31df74dbeaf..e2ae179676b 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -10,21 +10,21 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, PLATFORM_SCHEMA, - SUPPORT_PLAY, MediaPlayerDevice) + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, CONF_NAME, - CONF_HOST, CONF_PORT, CONF_SSL) + CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, STATE_IDLE, STATE_OFF, STATE_ON, + STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'iTunes' DEFAULT_PORT = 8181 -DEFAULT_TIMEOUT = 10 DEFAULT_SSL = False +DEFAULT_TIMEOUT = 10 DOMAIN = 'itunes' SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -52,7 +52,7 @@ class Itunes: @property def _base_url(self): - """Return the base url for endpoints.""" + """Return the base URL for endpoints.""" if self.use_ssl: uri_scheme = 'https://' else: diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index b36512e7c65..c98dc5c56fe 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -8,27 +8,29 @@ import asyncio from collections import OrderedDict from functools import wraps import logging +import re import socket import urllib -import re import aiohttp import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET, - MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_MOVIE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON) + DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) from homeassistant.const import ( - STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, - CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, - CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP) + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_PROXY_SSL, + CONF_TIMEOUT, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE, + STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import script from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers import script, config_validation as cv from homeassistant.helpers.template import Template from homeassistant.util.yaml import dump @@ -310,6 +312,8 @@ class KodiDevice(MediaPlayerDevice): # Register notification listeners self._ws_server.Player.OnPause = self.async_on_speed_event self._ws_server.Player.OnPlay = self.async_on_speed_event + self._ws_server.Player.OnAVStart = self.async_on_speed_event + self._ws_server.Player.OnAVChange = self.async_on_speed_event self._ws_server.Player.OnResume = self.async_on_speed_event self._ws_server.Player.OnSpeedChanged = self.async_on_speed_event self._ws_server.Player.OnStop = self.async_on_stop diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index cbc15af91b6..92f48411401 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -10,15 +10,16 @@ import logging from requests import RequestException import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MEDIA_TYPE_CHANNEL, MediaPlayerDevice) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_ACCESS_TOKEN, - STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) from homeassistant import util +from homeassistant.components.media_player import ( + MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, + STATE_PLAYING, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pylgnetcast-homeassistant==0.2.0.dev0'] @@ -35,16 +36,16 @@ SUPPORT_LGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_ACCESS_TOKEN): - vol.All(cv.string, vol.Length(max=6)), + vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LG TV platform.""" from pylgnetcast import LgNetCastClient + host = config.get(CONF_HOST) access_token = config.get(CONF_ACCESS_TOKEN) name = config.get(CONF_NAME) diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py index d5b1bcd78fe..9a08ceeac93 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -5,20 +5,20 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.liveboxplaytv/ """ import asyncio -import logging from datetime import timedelta +import logging import requests import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY, - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, SUPPORT_SELECT_SOURCE, - MEDIA_TYPE_CHANNEL, MediaPlayerDevice, PLATFORM_SCHEMA) + MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_PORT, STATE_ON, STATE_OFF, STATE_PLAYING, - STATE_PAUSED, CONF_NAME) + CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON, STATE_PAUSED, + STATE_PLAYING) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -40,7 +40,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py index f0b5365cf83..345b58cbbe4 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -8,45 +8,41 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, - SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, - SUPPORT_VOLUME_MUTE, MediaPlayerDevice, -) -from homeassistant.helpers.dispatcher import ( - dispatcher_send, async_dispatcher_connect -) + MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF, - CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, - STATE_UNAVAILABLE, EVENT_HOMEASSISTANT_STOP -) + CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, + EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY, STATE_UNAVAILABLE) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) REQUIREMENTS = ['pymediaroom==0.6.4'] _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Mediaroom STB' +DATA_MEDIAROOM = 'mediaroom_known_stb' +DEFAULT_NAME = "Mediaroom STB" DEFAULT_TIMEOUT = 9 -DATA_MEDIAROOM = "mediaroom_known_stb" -DISCOVERY_MEDIAROOM = "mediaroom_discovery_installed" +DISCOVERY_MEDIAROOM = 'mediaroom_discovery_installed' + SIGNAL_STB_NOTIFY = 'mediaroom_stb_discovered' SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF \ | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_PLAY_MEDIA \ | SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK \ | SUPPORT_PLAY -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } -) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, +}) async def async_setup_platform(hass, config, async_add_entities, @@ -57,10 +53,9 @@ async def async_setup_platform(hass, config, async_add_entities, known_hosts = hass.data[DATA_MEDIAROOM] = [] host = config.get(CONF_HOST, None) if host: - async_add_entities([MediaroomDevice(host=host, - device_id=None, - optimistic=config[CONF_OPTIMISTIC], - timeout=config[CONF_TIMEOUT])]) + async_add_entities([MediaroomDevice( + host=host, device_id=None, optimistic=config[CONF_OPTIMISTIC], + timeout=config[CONF_TIMEOUT])]) hass.data[DATA_MEDIAROOM].append(host) _LOGGER.debug("Trying to discover Mediaroom STB") @@ -75,8 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, hass.data[DATA_MEDIAROOM].append(notify.ip_address) new_stb = MediaroomDevice( host=notify.ip_address, device_id=notify.device_uuid, - optimistic=False - ) + optimistic=False) async_add_entities([new_stb]) if not config[CONF_OPTIMISTIC]: @@ -90,11 +84,11 @@ async def async_setup_platform(hass, config, async_add_entities, @callback def stop_discovery(event): """Stop discovery of new mediaroom STB's.""" - _LOGGER.debug("Stopping internal pymediaroom discovery.") + _LOGGER.debug("Stopping internal pymediaroom discovery") hass.data[DISCOVERY_MEDIAROOM].close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, - stop_discovery) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, stop_discovery) _LOGGER.debug("Auto discovery installed") @@ -118,8 +112,8 @@ class MediaroomDevice(MediaPlayerDevice): self._state = state_map[mediaroom_state] - def __init__(self, host, device_id, optimistic=False, - timeout=DEFAULT_TIMEOUT): + def __init__( + self, host, device_id, optimistic=False, timeout=DEFAULT_TIMEOUT): """Initialize the device.""" from pymediaroom import Remote @@ -160,8 +154,8 @@ class MediaroomDevice(MediaPlayerDevice): self._available = True self.async_schedule_update_ha_state() - async_dispatcher_connect(self.hass, SIGNAL_STB_NOTIFY, - async_notify_received) + async_dispatcher_connect( + self.hass, SIGNAL_STB_NOTIFY, async_notify_received) async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index 840429ef3d4..e6bc1f2699d 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -11,12 +11,12 @@ import requests import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_VOLUME_MUTE, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_NEXT_TRACK, - SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, - MediaPlayerDevice, PLATFORM_SCHEMA) + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - STATE_OFF, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, CONF_NAME, CONF_HOST, - CONF_PORT) + CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, + STATE_PLAYING) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/nad.py b/homeassistant/components/media_player/nad.py index d30c5815d3f..5fff8831617 100644 --- a/homeassistant/components/media_player/nad.py +++ b/homeassistant/components/media_player/nad.py @@ -9,12 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, - SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, - PLATFORM_SCHEMA) -from homeassistant.const import ( - CONF_NAME, STATE_OFF, STATE_ON) + PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['nad_receiver==0.0.9'] diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index af9a6ef54ce..0ba098d85f5 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -12,10 +12,10 @@ from typing import List # noqa: F401 import voluptuous as vol from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, - MediaPlayerDevice, PLATFORM_SCHEMA) -from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) + SUPPORT_VOLUME_STEP, MediaPlayerDevice) +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['onkyo-eiscp==1.2.4'] @@ -30,17 +30,19 @@ SUPPORTED_MAX_VOLUME = 80 SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA SUPPORT_ONKYO_WO_VOLUME = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA KNOWN_HOSTS = [] # type: List[str] DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', 'video1': 'Video 1', 'video2': 'Video 2', 'video3': 'Video 3', 'video4': 'Video 4', 'video5': 'Video 5', 'video6': 'Video 6', - 'video7': 'Video 7'} + 'video7': 'Video 7', 'fm': 'Radio'} + +DEFAULT_PLAYABLE_SOURCES = ("fm", "am", "tuner") PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, @@ -266,6 +268,13 @@ class OnkyoDevice(MediaPlayerDevice): source = self._reverse_mapping[source] self.command('input-selector {}'.format(source)) + def play_media(self, media_type, media_id, **kwargs): + """Play radio station by preset number.""" + source = self._reverse_mapping[self._current_source] + if (media_type.lower() == 'radio' and + source in DEFAULT_PLAYABLE_SOURCES): + self.command('preset {}'.format(media_id)) + class OnkyoDeviceZone(OnkyoDevice): """Representation of an Onkyo device's extra zone.""" diff --git a/homeassistant/components/media_player/openhome.py b/homeassistant/components/media_player/openhome.py index 1ffe0ef82df..ab23f8a7f9a 100644 --- a/homeassistant/components/media_player/openhome.py +++ b/homeassistant/components/media_player/openhome.py @@ -7,12 +7,12 @@ https://home-assistant.io/components/media_player.openhome/ import logging from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, SUPPORT_STOP, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF) + STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) REQUIREMENTS = ['openhomedevice==0.4.2'] diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 937a72c80ff..d3e56c4dfb1 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -9,22 +9,19 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MEDIA_TYPE_URL, - SUPPORT_PLAY_MEDIA, SUPPORT_STOP, - SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA) + MEDIA_TYPE_URL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT) + CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON, + STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['panasonic_viera==0.3.1', - 'wakeonlan==1.0.0'] +REQUIREMENTS = ['panasonic_viera==0.3.1', 'wakeonlan==1.1.6'] _LOGGER = logging.getLogger(__name__) -CONF_MAC = 'mac' - DEFAULT_NAME = 'Panasonic Viera TV' DEFAULT_PORT = 55000 diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py index 5295bfc40eb..231ea5302ae 100644 --- a/homeassistant/components/media_player/pandora.py +++ b/homeassistant/components/media_player/pandora.py @@ -4,23 +4,22 @@ Component for controlling Pandora stations through the pianobar client. For more details about this platform, please refer to the documentation https://home-assistant.io/components/media_player.pandora/ """ -import logging -import re -import os -import signal from datetime import timedelta +import logging +import os +import re import shutil +import signal -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, MEDIA_TYPE_MUSIC, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PLAY, - SUPPORT_SELECT_SOURCE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY_PAUSE, - SERVICE_MEDIA_PLAY, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, - MediaPlayerDevice) -from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING, - STATE_IDLE) from homeassistant import util +from homeassistant.components.media_player import ( + MEDIA_TYPE_MUSIC, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PLAY_PAUSE, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_PAUSED, + STATE_PLAYING) REQUIREMENTS = ['pexpect==4.6.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 9c7418d4c80..7d434ab480e 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -4,19 +4,19 @@ Media Player component to integrate TVs exposing the Joint Space API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.philips_js/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_PLAY, MediaPlayerDevice) + MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_API_VERSION, STATE_OFF, STATE_ON, STATE_UNKNOWN) + CONF_API_VERSION, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script from homeassistant.util import Throttle @@ -37,7 +37,7 @@ CONF_ON_ACTION = 'turn_on_action' DEFAULT_DEVICE = 'default' DEFAULT_HOST = '127.0.0.1' -DEFAULT_NAME = 'Philips TV' +DEFAULT_NAME = "Philips TV" DEFAULT_API_VERSION = '1' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py index 71ccf9a460d..29e4068f1d4 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -10,12 +10,12 @@ import telnetlib import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, + PLATFORM_SCHEMA, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_PLAY) + MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_NAME, CONF_PORT, - CONF_TIMEOUT) + CONF_HOST, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF, STATE_ON, + STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/pjlink.py b/homeassistant/components/media_player/pjlink.py index 42884cdce09..168cde4a792 100644 --- a/homeassistant/components/media_player/pjlink.py +++ b/homeassistant/components/media_player/pjlink.py @@ -9,11 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, - SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA, MediaPlayerDevice) + PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, MediaPlayerDevice) from homeassistant.const import ( - STATE_OFF, STATE_ON, CONF_HOST, - CONF_NAME, CONF_PASSWORD, CONF_PORT) + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pypjlink2==1.2.0'] @@ -87,8 +86,8 @@ class PjLinkDevice(MediaPlayerDevice): def projector(self): """Create PJLink Projector instance.""" from pypjlink import Projector - projector = Projector.from_address(self._host, self._port, - self._encoding) + projector = Projector.from_address( + self._host, self._port, self._encoding) projector.authenticate(self._password) return projector diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 46dacd98aad..0b4069ed664 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -4,17 +4,16 @@ Support to interface with the Plex API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.plex/ """ +from datetime import timedelta import json import logging -from datetime import timedelta - import requests import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA, + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) @@ -22,9 +21,8 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_utc_time_change -from homeassistant.util.json import load_json, save_json from homeassistant.util import dt as dt_util - +from homeassistant.util.json import load_json, save_json REQUIREMENTS = ['plexapi==3.0.6'] @@ -35,6 +33,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) PLEX_CONFIG_FILE = 'plex.conf' +PLEX_DATA = 'plex' CONF_INCLUDE_NON_CLIENTS = 'include_non_clients' CONF_USE_EPISODE_ART = 'use_episode_art' @@ -44,20 +43,14 @@ CONF_REMOVE_UNAVAILABLE_CLIENTS = 'remove_unavailable_clients' CONF_CLIENT_REMOVE_INTERVAL = 'client_remove_interval' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False): - cv.boolean, - vol.Optional(CONF_USE_EPISODE_ART, default=False): - cv.boolean, - vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False): - cv.boolean, - vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): - cv.boolean, + vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False): cv.boolean, + vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, + vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False): cv.boolean, + vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean, vol.Optional(CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600)): vol.All(cv.time_period, cv.positive_timedelta), }) -PLEX_DATA = "plex" - def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the Plex platform.""" @@ -157,8 +150,8 @@ def setup_plexserver( _LOGGER.exception("Error listing plex devices") return except requests.exceptions.RequestException as ex: - _LOGGER.error("Could not connect to plex server at http://%s (%s)", - host, ex) + _LOGGER.error( + "Could not connect to plex server at http://%s (%s)", host, ex) return new_plex_clients = [] @@ -171,9 +164,9 @@ def setup_plexserver( available_client_ids.append(device.machineIdentifier) if device.machineIdentifier not in plex_clients: - new_client = PlexClient(config, device, None, - plex_sessions, update_devices, - update_sessions) + new_client = PlexClient( + config, device, None, plex_sessions, update_devices, + update_sessions) plex_clients[device.machineIdentifier] = new_client new_plex_clients.append(new_client) else: @@ -184,9 +177,9 @@ def setup_plexserver( for machine_identifier, session in plex_sessions.items(): if (machine_identifier not in plex_clients and machine_identifier is not None): - new_client = PlexClient(config, None, session, - plex_sessions, update_devices, - update_sessions) + new_client = PlexClient( + config, None, session, plex_sessions, update_devices, + update_sessions) plex_clients[machine_identifier] = new_client new_plex_clients.append(new_client) else: @@ -225,8 +218,8 @@ def setup_plexserver( _LOGGER.exception("Error listing plex sessions") return except requests.exceptions.RequestException as ex: - _LOGGER.error("Could not connect to plex server at http://%s (%s)", - host, ex) + _LOGGER.error( + "Could not connect to plex server at http://%s (%s)", host, ex) return plex_sessions.clear() diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index fca7b29d2ec..fccca235193 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -9,11 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) + MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME) + CONF_HOST, STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['python-roku==3.1.5'] @@ -40,13 +40,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hosts = [] if discovery_info: - host = discovery_info.get("host") + host = discovery_info.get('host') if host in KNOWN_HOSTS: return _LOGGER.debug("Discovered Roku: %s", host) - hosts.append(discovery_info.get("host")) + hosts.append(discovery_info.get('host')) elif CONF_HOST in config: hosts.append(config.get(CONF_HOST)) diff --git a/homeassistant/components/media_player/russound_rio.py b/homeassistant/components/media_player/russound_rio.py index cf946945cf2..74f6bfb35ab 100644 --- a/homeassistant/components/media_player/russound_rio.py +++ b/homeassistant/components/media_player/russound_rio.py @@ -4,20 +4,19 @@ Support for Russound multizone controllers using RIO Protocol. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.russound_rio/ """ - import asyncio import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.media_player import ( - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, - MEDIA_TYPE_MUSIC) + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, - CONF_NAME, EVENT_HOMEASSISTANT_STOP) + CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, + STATE_ON) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['russound_rio==0.1.4'] @@ -31,26 +30,24 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=9621): cv.port, - }) +}) @asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Russound RIO platform.""" + from russound_rio import Russound + host = config.get(CONF_HOST) port = config.get(CONF_PORT) - from russound_rio import Russound - russ = Russound(hass.loop, host, port) yield from russ.connect() - # Discover sources + # Discover sources and zones sources = yield from russ.enumerate_sources() - - # Discover zones valid_zones = yield from russ.enumerate_zones() devices = [] @@ -81,9 +78,8 @@ class RussoundZoneDevice(MediaPlayerDevice): self._sources = sources def _zone_var(self, name, default=None): - return self._russ.get_cached_zone_variable(self._zone_id, - name, - default) + return self._russ.get_cached_zone_variable( + self._zone_id, name, default) def _source_var(self, name, default=None): current = int(self._zone_var('currentsource', 0)) @@ -188,21 +184,17 @@ class RussoundZoneDevice(MediaPlayerDevice): def async_turn_off(self): """Turn off the zone.""" - return self._russ.send_zone_event(self._zone_id, - "ZoneOff") + return self._russ.send_zone_event(self._zone_id, 'ZoneOff') def async_turn_on(self): """Turn on the zone.""" - return self._russ.send_zone_event(self._zone_id, - "ZoneOn") + return self._russ.send_zone_event(self._zone_id, 'ZoneOn') def async_set_volume_level(self, volume): """Set the volume level.""" rvol = int(volume * 50.0) - return self._russ.send_zone_event(self._zone_id, - "KeyPress", - "Volume", - rvol) + return self._russ.send_zone_event( + self._zone_id, 'KeyPress', 'Volume', rvol) def async_select_source(self, source): """Select the source input for this zone.""" @@ -210,4 +202,4 @@ class RussoundZoneDevice(MediaPlayerDevice): if name.lower() != source.lower(): continue return self._russ.send_zone_event( - self._zone_id, "SelectSource", source_id) + self._zone_id, 'SelectSource', source_id) diff --git a/homeassistant/components/media_player/russound_rnet.py b/homeassistant/components/media_player/russound_rnet.py index 8aef15e02af..7f4d04eb634 100644 --- a/homeassistant/components/media_player/russound_rnet.py +++ b/homeassistant/components/media_player/russound_rnet.py @@ -9,10 +9,10 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) + PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME) + CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['russound==0.1.9'] diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 72c3ab2c621..c0a5d617f19 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -5,37 +5,34 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.samsungtv/ """ import asyncio +from datetime import timedelta import logging import socket -from datetime import timedelta - +import subprocess import sys -import subprocess import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PLAY, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY_MEDIA, - MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_CHANNEL) + MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT, - CONF_MAC) + CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_TIMEOUT, STATE_OFF, + STATE_ON, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util -REQUIREMENTS = ['samsungctl[websocket]==0.7.1', 'wakeonlan==1.0.0'] +REQUIREMENTS = ['samsungctl[websocket]==0.7.1', 'wakeonlan==1.1.6'] _LOGGER = logging.getLogger(__name__) -CONF_TIMEOUT = 'timeout' - DEFAULT_NAME = 'Samsung TV Remote' DEFAULT_PORT = 55000 DEFAULT_TIMEOUT = 0 -KEY_PRESS_TIMEOUT = 1.2 +KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = 'samsungtv_known_devices' SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ diff --git a/homeassistant/components/media_player/sisyphus.py b/homeassistant/components/media_player/sisyphus.py index 36f28769b12..ef6b02514f0 100644 --- a/homeassistant/components/media_player/sisyphus.py +++ b/homeassistant/components/media_player/sisyphus.py @@ -7,25 +7,18 @@ https://home-assistant.io/components/media_player.sisyphus/ import logging from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SHUFFLE_SET, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.components.sisyphus import DATA_SISYPHUS -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_PLAYING, \ - STATE_PAUSED, STATE_IDLE, STATE_OFF +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['sisyphus'] -MEDIA_TYPE_TRACK = "sisyphus_track" +MEDIA_TYPE_TRACK = 'sisyphus_track' SUPPORTED_FEATURES = SUPPORT_VOLUME_MUTE \ | SUPPORT_VOLUME_SET \ @@ -44,21 +37,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = discovery_info[CONF_NAME] host = discovery_info[CONF_HOST] add_entities( - [SisyphusPlayer(name, host, hass.data[DATA_SISYPHUS][name])], - update_before_add=True) + [SisyphusPlayer(name, host, hass.data[DATA_SISYPHUS][name])], True) class SisyphusPlayer(MediaPlayerDevice): - """Represents a single Sisyphus table as a media player device.""" + """Representation of a Sisyphus table as a media player device.""" def __init__(self, name, host, table): - """ - Constructor. - - :param name: name of the table - :param host: hostname or ip address - :param table: sisyphus-control Table object - """ + """Initialize the Sisyphus media device.""" self._name = name self._host = host self._table = table @@ -99,11 +85,7 @@ class SisyphusPlayer(MediaPlayerDevice): return self._table.is_shuffle async def async_set_shuffle(self, shuffle): - """ - Change the shuffle mode of the current playlist. - - :param shuffle: True to shuffle, False not to - """ + """Change the shuffle mode of the current playlist.""" await self._table.set_shuffle(shuffle) @property diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index e45819428e8..83b10997c31 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -9,32 +9,30 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, - SUPPORT_TURN_ON, MediaPlayerDevice, DOMAIN) -from homeassistant.const import ( - CONF_NAME, STATE_ON, STATE_OFF, ATTR_ENTITY_ID) + DOMAIN, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['python-songpal==0.0.8'] +_LOGGER = logging.getLogger(__name__) + +CONF_ENDPOINT = 'endpoint' + +PARAM_NAME = 'name' +PARAM_VALUE = 'value' + +PLATFORM = 'songpal' + +SET_SOUND_SETTING = 'songpal_set_sound_setting' + SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF -_LOGGER = logging.getLogger(__name__) - - -PLATFORM = "songpal" - -SET_SOUND_SETTING = "songpal_set_sound_setting" - -PARAM_NAME = "name" -PARAM_VALUE = "value" - -CONF_ENDPOINT = "endpoint" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENDPOINT): cv.string, @@ -43,13 +41,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ SET_SOUND_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_id, vol.Required(PARAM_NAME): cv.string, - vol.Required(PARAM_VALUE): cv.string}) + vol.Required(PARAM_VALUE): cv.string, +}) -async def async_setup_platform(hass, config, - async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Songpal platform.""" from songpal import SongpalException + if PLATFORM not in hass.data: hass.data[PLATFORM] = {} @@ -85,8 +85,8 @@ async def async_setup_platform(hass, config, _LOGGER.debug("Calling %s (entity: %s) with params %s", service, entity_id, params) - await device.async_set_sound_setting(params[PARAM_NAME], - params[PARAM_VALUE]) + await device.async_set_sound_setting( + params[PARAM_NAME], params[PARAM_VALUE]) hass.services.async_register( DOMAIN, SET_SOUND_SETTING, async_service_handler, @@ -151,8 +151,8 @@ class SongpalDevice(MediaPlayerDevice): return if len(volumes) > 1: - _LOGGER.debug("Got %s volume controls, using the first one", - volumes) + _LOGGER.debug( + "Got %s volume controls, using the first one", volumes) volume = volumes[0] _LOGGER.debug("Current volume: %s", volume) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 4fc6b8b0954..1486d47e759 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -1,5 +1,5 @@ """ -Support to interface with Sonos players (via SoCo). +Support to interface with Sonos players. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.sonos/ @@ -9,8 +9,8 @@ import datetime import functools as ft import logging import socket -import urllib import threading +import urllib import voluptuous as vol @@ -31,11 +31,9 @@ DEPENDENCIES = ('sonos',) _LOGGER = logging.getLogger(__name__) -# Quiet down soco logging to just actual problems. -logging.getLogger('soco').setLevel(logging.WARNING) -logging.getLogger('soco.events').setLevel(logging.ERROR) -logging.getLogger('soco.data_structures_entry').setLevel(logging.ERROR) -_SOCO_SERVICES_LOGGER = logging.getLogger('soco.services') +# Quiet down pysonos logging to just actual problems. +logging.getLogger('pysonos').setLevel(logging.WARNING) +logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR) SUPPORT_SONOS = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_SELECT_SOURCE |\ @@ -143,18 +141,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def _setup_platform(hass, config, add_entities, discovery_info): """Set up the Sonos platform.""" - import soco + import pysonos if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() advertise_addr = config.get(CONF_ADVERTISE_ADDR) if advertise_addr: - soco.config.EVENT_ADVERTISE_IP = advertise_addr + pysonos.config.EVENT_ADVERTISE_IP = advertise_addr players = [] if discovery_info: - player = soco.SoCo(discovery_info.get('host')) + player = pysonos.SoCo(discovery_info.get('host')) # If device already exists by config if player.uid in hass.data[DATA_SONOS].uids: @@ -174,11 +172,11 @@ def _setup_platform(hass, config, add_entities, discovery_info): hosts = hosts.split(',') if isinstance(hosts, str) else hosts for host in hosts: try: - players.append(soco.SoCo(socket.gethostbyname(host))) + players.append(pysonos.SoCo(socket.gethostbyname(host))) except OSError: _LOGGER.warning("Failed to initialize '%s'", host) else: - players = soco.discover( + players = pysonos.discover( interface_addr=config.get(CONF_INTERFACE_ADDR)) if not players: @@ -287,11 +285,7 @@ def soco_error(errorcodes=None): @ft.wraps(funct) def wrapper(*args, **kwargs): """Wrap for all soco UPnP exception.""" - from soco.exceptions import SoCoUPnPException, SoCoException - - # Temporarily disable SoCo logging because it will log the - # UPnP exception otherwise - _SOCO_SERVICES_LOGGER.disabled = True + from pysonos.exceptions import SoCoUPnPException, SoCoException try: return funct(*args, **kwargs) @@ -302,8 +296,6 @@ def soco_error(errorcodes=None): _LOGGER.error("Error on %s with %s", funct.__name__, err) except SoCoException as err: _LOGGER.error("Error on %s with %s", funct.__name__, err) - finally: - _SOCO_SERVICES_LOGGER.disabled = False return wrapper return decorator @@ -612,9 +604,9 @@ class SonosDevice(MediaPlayerDevice): current_uri_metadata = media_info["CurrentURIMetaData"] if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None): # currently soco does not have an API for this - import soco - current_uri_metadata = soco.xml.XML.fromstring( - soco.utils.really_utf8(current_uri_metadata)) + import pysonos + current_uri_metadata = pysonos.xml.XML.fromstring( + pysonos.utils.really_utf8(current_uri_metadata)) md_title = current_uri_metadata.findtext( './/{http://purl.org/dc/elements/1.1/}title') @@ -884,6 +876,8 @@ class SonosDevice(MediaPlayerDevice): sources += [SOURCE_LINEIN] elif 'PLAYBAR' in model: sources += [SOURCE_LINEIN, SOURCE_TV] + elif 'BEAM' in model: + sources += [SOURCE_TV] return sources @@ -948,7 +942,7 @@ class SonosDevice(MediaPlayerDevice): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ if kwargs.get(ATTR_MEDIA_ENQUEUE): - from soco.exceptions import SoCoUPnPException + from pysonos.exceptions import SoCoUPnPException try: self.soco.add_uri_to_queue(media_id) except SoCoUPnPException: @@ -979,7 +973,7 @@ class SonosDevice(MediaPlayerDevice): @soco_error() def snapshot(self, with_group=True): """Snapshot the player.""" - from soco.snapshot import Snapshot + from pysonos.snapshot import Snapshot self._soco_snapshot = Snapshot(self.soco) self._soco_snapshot.snapshot() @@ -994,7 +988,7 @@ class SonosDevice(MediaPlayerDevice): @soco_error() def restore(self, with_group=True): """Restore snapshot for the player.""" - from soco.exceptions import SoCoException + from pysonos.exceptions import SoCoException try: # need catch exception if a coordinator is going to slave. # this state will recover with group part. @@ -1058,7 +1052,7 @@ class SonosDevice(MediaPlayerDevice): @soco_coordinator def set_alarm(self, **data): """Set the alarm clock on the player.""" - from soco import alarms + from pysonos import alarms alarm = None for one_alarm in alarms.get_alarms(self.soco): # pylint: disable=protected-access diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 4e26af9dcc2..037a9b88fc6 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -5,19 +5,19 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.soundtouch/ """ import logging - import re + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_VOLUME_SET, SUPPORT_TURN_ON, SUPPORT_PLAY, MediaPlayerDevice, - DOMAIN, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT, - STATE_PAUSED, STATE_PLAYING, - STATE_UNAVAILABLE) + DOMAIN, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING, + STATE_UNAVAILABLE) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['libsoundtouch==0.7.2'] @@ -43,17 +43,17 @@ SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({ SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({ vol.Required('master'): cv.entity_id, - vol.Required('slaves'): cv.entity_ids + vol.Required('slaves'): cv.entity_ids, }) SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({ vol.Required('master'): cv.entity_id, - vol.Required('slaves'): cv.entity_ids + vol.Required('slaves'): cv.entity_ids, }) SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({ vol.Required('master'): cv.entity_id, - vol.Required('slaves'): cv.entity_ids + vol.Required('slaves'): cv.entity_ids, }) DEFAULT_NAME = 'Bose Soundtouch' @@ -67,7 +67,7 @@ SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) @@ -297,7 +297,7 @@ class SoundTouchDevice(MediaPlayerDevice): def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Starting media with media_id: %s", media_id) - if re.match(r'http://', str(media_id)): + if re.match(r'http?://', str(media_id)): # URL _LOGGER.debug("Playing URL %s", str(media_id)) self._device.play_url(str(media_id)) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 9fc200c67fd..8a4ffeeb157 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -4,48 +4,54 @@ Support for interacting with Spotify Connect. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.spotify/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_VOLUME_SET, - SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_NEXT_TRACK, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, - PLATFORM_SCHEMA, MediaPlayerDevice) + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, + MediaPlayerDevice) from homeassistant.const import ( - CONF_NAME, STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_UNKNOWN) + CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['spotipy-homeassistant==2.4.4.dev1'] -DEPENDENCIES = ['http'] - _LOGGER = logging.getLogger(__name__) +AUTH_CALLBACK_NAME = 'api:spotify' +AUTH_CALLBACK_PATH = '/api/spotify' + +CONF_ALIASES = 'aliases' +CONF_CACHE_PATH = 'cache_path' +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' + +CONFIGURATOR_DESCRIPTION = 'To link your Spotify account, ' \ + 'click the link, login, and authorize:' +CONFIGURATOR_LINK_NAME = 'Link Spotify account' +CONFIGURATOR_SUBMIT_CAPTION = 'I authorized successfully' + +DEFAULT_CACHE_PATH = '.spotify-token-cache' +DEFAULT_NAME = 'Spotify' +DEPENDENCIES = ['http'] +DOMAIN = 'spotify' + +ICON = 'mdi:spotify' + +SCAN_INTERVAL = timedelta(seconds=30) + +SCOPE = 'user-read-playback-state user-modify-playback-state user-read-private' + SUPPORT_SPOTIFY = SUPPORT_VOLUME_SET | SUPPORT_PAUSE | SUPPORT_PLAY |\ SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | SUPPORT_SELECT_SOURCE |\ SUPPORT_PLAY_MEDIA | SUPPORT_SHUFFLE_SET -SCOPE = 'user-read-playback-state user-modify-playback-state user-read-private' -DEFAULT_CACHE_PATH = '.spotify-token-cache' -AUTH_CALLBACK_PATH = '/api/spotify' -AUTH_CALLBACK_NAME = 'api:spotify' -ICON = 'mdi:spotify' -DEFAULT_NAME = 'Spotify' -DOMAIN = 'spotify' -CONF_ALIASES = 'aliases' -CONF_CLIENT_ID = 'client_id' -CONF_CLIENT_SECRET = 'client_secret' -CONF_CACHE_PATH = 'cache_path' -CONFIGURATOR_LINK_NAME = 'Link Spotify account' -CONFIGURATOR_SUBMIT_CAPTION = 'I authorized successfully' -CONFIGURATOR_DESCRIPTION = 'To link your Spotify account, ' \ - 'click the link, login, and authorize:' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, @@ -54,8 +60,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ALIASES, default={}): {cv.string: cv.string} }) -SCAN_INTERVAL = timedelta(seconds=30) - def request_configuration(hass, config, add_entities, oauth): """Request Spotify authorization.""" @@ -71,6 +75,7 @@ def request_configuration(hass, config, add_entities, oauth): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Spotify platform.""" import spotipy.oauth2 + callback_url = '{}{}'.format(hass.config.api.base_url, AUTH_CALLBACK_PATH) cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) oauth = spotipy.oauth2.SpotifyOAuth( @@ -88,8 +93,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): configurator = hass.components.configurator configurator.request_done(hass.data.get(DOMAIN)) del hass.data[DOMAIN] - player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME), - config[CONF_ALIASES]) + player = SpotifyMediaPlayer( + oauth, config.get(CONF_NAME, DEFAULT_NAME), config[CONF_ALIASES]) add_entities([player], True) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index a2732b5f849..2d6a849aecb 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -4,26 +4,26 @@ Support for interfacing to the Logitech SqueezeBox API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.squeezebox/ """ -import logging import asyncio -import urllib.parse import json +import logging +import urllib.parse + import aiohttp import async_timeout - import voluptuous as vol from homeassistant.components.media_player import ( - ATTR_MEDIA_ENQUEUE, SUPPORT_PLAY_MEDIA, - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice, - MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_SHUFFLE_SET, SUPPORT_CLEAR_PLAYLIST) + ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_MUSIC, + PLATFORM_SCHEMA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_IDLE, STATE_OFF, - STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, CONF_PORT, ATTR_COMMAND) -import homeassistant.helpers.config_validation as cv + ATTR_COMMAND, CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, + STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/ue_smart_radio.py b/homeassistant/components/media_player/ue_smart_radio.py index ae7617ead24..066972aaa25 100644 --- a/homeassistant/components/media_player/ue_smart_radio.py +++ b/homeassistant/components/media_player/ue_smart_radio.py @@ -6,31 +6,34 @@ https://home-assistant.io/components/media_player.ue_smart_radio/ """ import logging -import voluptuous as vol + import requests +import voluptuous as vol from homeassistant.components.media_player import ( - MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, - SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, - SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_MUTE) + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + MediaPlayerDevice) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, STATE_OFF, STATE_IDLE, STATE_PLAYING, - STATE_PAUSED) + CONF_PASSWORD, CONF_USERNAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, + STATE_PLAYING) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ICON = "mdi:radio" -URL = "http://decibel.logitechmusic.com/jsonrpc.js" +ICON = 'mdi:radio' +URL = 'http://decibel.logitechmusic.com/jsonrpc.js' SUPPORT_UE_SMART_RADIO = SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_TURN_ON | \ SUPPORT_TURN_OFF | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE -PLAYBACK_DICT = {"play": STATE_PLAYING, - "pause": STATE_PAUSED, - "stop": STATE_IDLE} +PLAYBACK_DICT = { + 'play': STATE_PLAYING, + 'pause': STATE_PAUSED, + 'stop': STATE_IDLE, +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, @@ -41,10 +44,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def send_request(payload, session): """Send request to radio.""" try: - request = requests.post(URL, - cookies={"sdi_squeezenetwork_session": - session}, - json=payload, timeout=5) + request = requests.post( + URL, cookies={"sdi_squeezenetwork_session": session}, + json=payload, timeout=5) except requests.exceptions.Timeout: _LOGGER.error("Timed out when sending request") except requests.exceptions.ConnectionError: @@ -58,9 +60,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): email = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - session_request = requests.post("https://www.uesmartradio.com/user/login", - data={"email": email, "password": - password}) + session_request = requests.post( + "https://www.uesmartradio.com/user/login", + data={"email": email, "password": password}, timeout=5) session = session_request.cookies["sdi_squeezenetwork_session"] player_request = send_request({"params": ["", ["serverstatus"]]}, session) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 1572e2df89b..47eaf599929 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -4,12 +4,11 @@ Combination of multiple media players into one for a universal controller. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ -import logging from copy import copy +import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.media_player import ( ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, @@ -18,36 +17,35 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL, - ATTR_MEDIA_VOLUME_MUTED, DOMAIN, MediaPlayerDevice, PLATFORM_SCHEMA, - SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, - SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP) + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, PLATFORM_SCHEMA, SERVICE_CLEAR_PLAYLIST, + SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, CONF_NAME, - CONF_STATE_TEMPLATE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, - SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, - SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, - SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, - SERVICE_MEDIA_STOP) + CONF_STATE, CONF_STATE_TEMPLATE, SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, + SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, + SERVICE_SHUFFLE_SET, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_call_from_config +_LOGGER = logging.getLogger(__name__) + ATTR_ACTIVE_CHILD = 'active_child' +ATTR_DATA = 'data' CONF_ATTRS = 'attributes' CONF_CHILDREN = 'children' CONF_COMMANDS = 'commands' -CONF_PLATFORM = 'platform' CONF_SERVICE = 'service' CONF_SERVICE_DATA = 'service_data' -ATTR_DATA = 'data' -CONF_STATE = 'state' OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE] -REQUIREMENTS = [] -_LOGGER = logging.getLogger(__name__) ATTRS_SCHEMA = vol.Schema({cv.slug: cv.string}) CMD_SCHEMA = vol.Schema({cv.slug: cv.SERVICE_SCHEMA}) diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 673be3074de..9564a8d3df0 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -9,6 +9,7 @@ import logging import voluptuous as vol +from homeassistant import util from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -18,7 +19,6 @@ from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) from homeassistant.helpers import config_validation as cv -from homeassistant import util REQUIREMENTS = ['pyvizio==0.0.3'] diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py index 075a533e372..5cc4196d4e1 100644 --- a/homeassistant/components/media_player/vlc.py +++ b/homeassistant/components/media_player/vlc.py @@ -9,12 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, - MEDIA_TYPE_MUSIC) - -from homeassistant.const import (CONF_NAME, STATE_IDLE, STATE_PAUSED, - STATE_PLAYING) + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -29,8 +28,8 @@ SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_STOP PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ARGUMENTS, default=''): cv.string, + vol.Optional(CONF_NAME): cv.string, }) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 00f5d25362f..743f19cb259 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -6,23 +6,24 @@ https://home-assistant.io/components/media_player.volumio/ Volumio rest API: https://volumio.github.io/docs/API/REST_API.html """ +import asyncio from datetime import timedelta import logging import socket -import asyncio -import aiohttp +import aiohttp import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, - SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, - SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST) + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) from homeassistant.const import ( - STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOST, CONF_PORT, CONF_NAME) -import homeassistant.helpers.config_validation as cv + CONF_HOST, CONF_NAME, CONF_PORT, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _CONFIGURING = {} diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index fd6b1c6d96e..0a5b9fe509b 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -8,12 +8,11 @@ import asyncio from datetime import timedelta import logging from urllib.parse import urlparse - -# pylint: disable=unused-import -from typing import Dict # noqa: F401 +from typing import Dict # noqa: F401 pylint: disable=unused-import import voluptuous as vol +from homeassistant import util from homeassistant.components.media_player import ( MEDIA_TYPE_CHANNEL, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, @@ -24,9 +23,8 @@ from homeassistant.const import ( STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -from homeassistant import util -REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2'] +REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==6.0'] _CONFIGURING = {} # type: Dict[str, str] _LOGGER = logging.getLogger(__name__) @@ -34,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' CONF_ON_ACTION = 'turn_on_action' -DEFAULT_NAME = 'LG webOS Smart TV' +DEFAULT_NAME = "LG webOS Smart TV" LIVETV_APP_ID = 'com.webos.app.livetv' WEBOSTV_CONFIG_FILE = 'webostv.conf' diff --git a/homeassistant/components/media_player/xiaomi_tv.py b/homeassistant/components/media_player/xiaomi_tv.py index ad66ae855bd..93c4bb72b17 100644 --- a/homeassistant/components/media_player/xiaomi_tv.py +++ b/homeassistant/components/media_player/xiaomi_tv.py @@ -4,14 +4,15 @@ Add support for the Xiaomi TVs. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/xiaomi_tv/ """ - import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) + from homeassistant.components.media_player import ( - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, MediaPlayerDevice, PLATFORM_SCHEMA, - SUPPORT_VOLUME_STEP) + PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pymitv==1.4.0'] @@ -41,8 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Check if there's a valid TV at the IP address. if not Discover().check_ip(host): _LOGGER.error( - "Could not find Xiaomi TV with specified IP: %s", host - ) + "Could not find Xiaomi TV with specified IP: %s", host) else: # Register TV with Home Assistant. add_entities([XiaomiTV(host, name)]) diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 2ffe58b02af..be61560d52b 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant.components.media_player import ( DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PLAYING) @@ -43,7 +43,8 @@ ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ SERVICE_ENABLE_OUTPUT = 'yamaha_enable_output' SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY \ + | SUPPORT_SELECT_SOUND_MODE PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -140,6 +141,8 @@ class YamahaDevice(MediaPlayerDevice): self._volume = 0 self._pwstate = STATE_OFF self._current_source = None + self._sound_mode = None + self._sound_mode_list = None self._source_list = None self._source_ignore = source_ignore or [] self._source_names = source_names or {} @@ -181,6 +184,8 @@ class YamahaDevice(MediaPlayerDevice): self._playback_support = self.receiver.get_playback_support() self._is_playback_supported = self.receiver.is_playback_supported( self._current_source) + self._sound_mode = self.receiver.surround_program + self._sound_mode_list = self.receiver.surround_programs() def build_source_list(self): """Build the source list.""" @@ -222,6 +227,16 @@ class YamahaDevice(MediaPlayerDevice): """Return the current input source.""" return self._current_source + @property + def sound_mode(self): + """Return the current sound mode.""" + return self._sound_mode + + @property + def sound_mode_list(self): + """Return the current sound mode.""" + return self._sound_mode_list + @property def source_list(self): """List of available input sources.""" @@ -330,6 +345,10 @@ class YamahaDevice(MediaPlayerDevice): """Enable or disable an output port..""" self.receiver.enable_output(port, enabled) + def select_sound_mode(self, sound_mode): + """Set Sound Mode for Receiver..""" + self.receiver.surround_program = sound_mode + @property def media_artist(self): """Artist of current playing media.""" diff --git a/homeassistant/components/media_player/yamaha_musiccast.py b/homeassistant/components/media_player/yamaha_musiccast.py index 135bf4d0aef..bf21a3f5028 100644 --- a/homeassistant/components/media_player/yamaha_musiccast.py +++ b/homeassistant/components/media_player/yamaha_musiccast.py @@ -1,28 +1,26 @@ -"""Example for configuration.yaml. - -media_player: - - platform: yamaha_musiccast - host: 192.168.xxx.xx - port: 5005 - """ +Support for Yamaha MusicCast Receivers. +For more details about this platform, please refer to the documentation at +https://www.home-assistant.io/components/media_player.yamaha_musiccast/ +""" import logging + import voluptuous as vol + +from homeassistant.components.media_player import ( + MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_PORT, STATE_IDLE, STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from homeassistant.const import ( - CONF_HOST, CONF_PORT, - STATE_UNKNOWN, STATE_ON, STATE_PLAYING, STATE_PAUSED, STATE_IDLE -) -from homeassistant.components.media_player import ( - MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, - SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY, - SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, - SUPPORT_SELECT_SOURCE, SUPPORT_STOP -) +REQUIREMENTS = ['pymusiccast==0.1.6'] + _LOGGER = logging.getLogger(__name__) SUPPORTED_FEATURES = ( @@ -36,8 +34,6 @@ SUPPORTED_FEATURES = ( KNOWN_HOSTS_KEY = 'data_yamaha_musiccast' INTERVAL_SECONDS = 'interval_seconds' -REQUIREMENTS = ['pymusiccast==0.1.6'] - DEFAULT_PORT = 5005 DEFAULT_INTERVAL = 480 @@ -71,11 +67,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return if [item for item in known_hosts if item[0] == ipaddr]: - _LOGGER.warning("Host %s:%d already registered.", host, port) + _LOGGER.warning("Host %s:%d already registered", host, port) return if [item for item in known_hosts if item[1] == port]: - _LOGGER.warning("Port %s:%d already registered.", host, port) + _LOGGER.warning("Port %s:%d already registered", host, port) return reg_host = (ipaddr, port) @@ -91,11 +87,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if receiver: for zone in receiver.zones: _LOGGER.debug( - "receiver: %s / Port: %d / Zone: %s", - receiver, port, zone) + "Receiver: %s / Port: %d / Zone: %s", receiver, port, zone) add_entities( - [YamahaDevice(receiver, receiver.zones[zone])], - True) + [YamahaDevice(receiver, receiver.zones[zone])], True) else: known_hosts.remove(reg_host) diff --git a/homeassistant/components/media_player/ziggo_mediabox_xl.py b/homeassistant/components/media_player/ziggo_mediabox_xl.py index 376b9e7c426..555042bee5c 100644 --- a/homeassistant/components/media_player/ziggo_mediabox_xl.py +++ b/homeassistant/components/media_player/ziggo_mediabox_xl.py @@ -10,10 +10,9 @@ import socket import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, MediaPlayerDevice, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, - SUPPORT_PLAY, SUPPORT_PAUSE) + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv @@ -169,6 +168,5 @@ class ZiggoMediaboxXLDevice(MediaPlayerDevice): if digits is None: return - self.send_keys(['NUM_{}'.format(digit) - for digit in str(digits)]) + self.send_keys(['NUM_{}'.format(digit) for digit in str(digits)]) self._state = STATE_PLAYING diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json new file mode 100644 index 00000000000..b6c73f35f26 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 de MQTT." + }, + "error": { + "cannot_connect": "No es pot connectar amb el broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "Activar descobreix automaticament", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "description": "Introdu\u00efu la informaci\u00f3 de connexi\u00f3 del vostre broker MQTT.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json new file mode 100644 index 00000000000..2a35e95f559 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Nur eine einzige Konfiguration von MQTT ist zul\u00e4ssig." + }, + "error": { + "cannot_connect": "Es konnte keine Verbindung zum Broker hergestellt werden." + }, + "step": { + "broker": { + "data": { + "discovery": "Suche aktivieren", + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json new file mode 100644 index 00000000000..c0b83a1323f --- /dev/null +++ b/homeassistant/components/mqtt/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of MQTT is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to the broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "Enable discovery", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "description": "Please enter the connection information of your MQTT broker.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/fr.json b/homeassistant/components/mqtt/.translations/fr.json new file mode 100644 index 00000000000..916b4fdaf39 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Une seule configuration de MQTT est autoris\u00e9e." + }, + "error": { + "cannot_connect": "Impossible de se connecter au broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "Activer la d\u00e9couverte automatique", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/he.json b/homeassistant/components/mqtt/.translations/he.json new file mode 100644 index 00000000000..e1e2ed49748 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc MQTT \u05de\u05d5\u05ea\u05e8\u05ea." + }, + "error": { + "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d1\u05e8\u05d5\u05e7\u05e8." + }, + "step": { + "broker": { + "data": { + "broker": "\u05d1\u05e8\u05d5\u05e7\u05e8", + "discovery": "\u05d0\u05e4\u05e9\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05d5\u05e8\u05d8", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json new file mode 100644 index 00000000000..d85814e917c --- /dev/null +++ b/homeassistant/components/mqtt/.translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Csak egyetlen MQTT konfigur\u00e1ci\u00f3 megengedett." + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni a br\u00f3kerhez." + }, + "step": { + "broker": { + "data": { + "broker": "Br\u00f3ker", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/id.json b/homeassistant/components/mqtt/.translations/id.json new file mode 100644 index 00000000000..7a9bf8639e2 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Hanya satu konfigurasi MQTT yang diizinkan." + }, + "error": { + "cannot_connect": "Tidak dapat terhubung ke broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Kata sandi", + "port": "Port", + "username": "Nama pengguna" + }, + "description": "Harap masukkan informasi koneksi dari broker MQTT Anda.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json new file mode 100644 index 00000000000..f20658d252c --- /dev/null +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 MQTT \ube0c\ub85c\ucee4\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "broker": { + "data": { + "broker": "\ube0c\ub85c\ucee4", + "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/lb.json b/homeassistant/components/mqtt/.translations/lb.json new file mode 100644 index 00000000000..166fce9fbfb --- /dev/null +++ b/homeassistant/components/mqtt/.translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vum MQTT ass erlaabt" + }, + "error": { + "cannot_connect": "Kann sech net mam Broker verbannen." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "Entdeckung aktiv\u00e9ieren", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "description": "Gitt Verbindungs Informatioune vun \u00e4rem MQTT Broker an.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/nl.json b/homeassistant/components/mqtt/.translations/nl.json new file mode 100644 index 00000000000..b375f353810 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "description": "MQTT", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/nn.json b/homeassistant/components/mqtt/.translations/nn.json new file mode 100644 index 00000000000..fb650bc7676 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/nn.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Det er berre lov \u00e5 ha \u00e9in MQTT-konfigurasjon" + }, + "error": { + "cannot_connect": "Klarte ikkje \u00e5 kople til meglaren." + }, + "step": { + "broker": { + "data": { + "broker": "Meglar", + "password": "Passord", + "port": "Port", + "username": "Brukarnamn" + }, + "description": "Ver vennleg \u00e5 skriv inn tilkoplingsinformasjonen for MQTT-meglaren din", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json new file mode 100644 index 00000000000..412efd3e107 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Kun en enkelt konfigurasjon av MQTT er tillatt." + }, + "error": { + "cannot_connect": "Kan ikke koble til megleren." + }, + "step": { + "broker": { + "data": { + "broker": "Megler", + "discovery": "Aktiver oppdagelse", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/pl.json b/homeassistant/components/mqtt/.translations/pl.json new file mode 100644 index 00000000000..e87e550b98d --- /dev/null +++ b/homeassistant/components/mqtt/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja MQTT." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z po\u015brednikiem." + }, + "step": { + "broker": { + "data": { + "broker": "Po\u015brednik", + "discovery": "W\u0142\u0105cz wykrywanie", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/pt-BR.json b/homeassistant/components/mqtt/.translations/pt-BR.json new file mode 100644 index 00000000000..e73e8b155ec --- /dev/null +++ b/homeassistant/components/mqtt/.translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do MQTT \u00e9 permitida." + }, + "step": { + "broker": { + "data": { + "password": "Senha", + "port": "Porta", + "username": "Nome de usu\u00e1rio" + }, + "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/pt.json b/homeassistant/components/mqtt/.translations/pt.json new file mode 100644 index 00000000000..42f7c7f5ad2 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do MQTT \u00e9 permitida." + }, + "error": { + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar ao broker." + }, + "step": { + "broker": { + "data": { + "broker": "", + "password": "Palavra-passe", + "port": "Porto", + "username": "Utilizador" + }, + "description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT.", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json new file mode 100644 index 00000000000..f1ff498dd72 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0414\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f MQTT." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443." + }, + "step": { + "broker": { + "data": { + "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", + "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0432\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json new file mode 100644 index 00000000000..a12498ac4c2 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Dovoljena je samo ena konfiguracija MQTT." + }, + "error": { + "cannot_connect": "Ne morem se povezati na posrednik." + }, + "step": { + "broker": { + "data": { + "broker": "Posrednik", + "discovery": "Omogo\u010di odkrivanje", + "password": "Geslo", + "port": "port", + "username": "Uporabni\u0161ko ime" + }, + "description": "Prosimo vnesite informacije o povezavi va\u0161ega MQTT posrednika.", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/sv.json b/homeassistant/components/mqtt/.translations/sv.json new file mode 100644 index 00000000000..7cf6d75b9c1 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "broker": { + "data": { + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/zh-Hans.json b/homeassistant/components/mqtt/.translations/zh-Hans.json new file mode 100644 index 00000000000..98a7d9eb4be --- /dev/null +++ b/homeassistant/components/mqtt/.translations/zh-Hans.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u53ea\u5141\u8bb8\u4e00\u4e2a MQTT \u914d\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u3002" + }, + "step": { + "broker": { + "data": { + "broker": "\u670d\u52a1\u5668", + "discovery": "\u542f\u7528\u53d1\u73b0", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d" + }, + "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/zh-Hant.json b/homeassistant/components/mqtt/.translations/zh-Hant.json new file mode 100644 index 00000000000..cf87ceb8f98 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 MQTT\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Broker\u3002" + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "\u958b\u555f\u63a2\u7d22", + "password": "\u4f7f\u7528\u8005\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002", + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 71be9c2435e..335b4d31acb 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -6,49 +6,54 @@ https://home-assistant.io/components/mqtt/ """ import asyncio from itertools import groupby -from typing import Optional, Any, Union, Callable, List, cast # noqa: F401 -from operator import attrgetter import logging +from operator import attrgetter import os import socket -import time import ssl -import re -import requests.certs -import attr +import time +from typing import Any, Callable, List, Optional, Union, cast # noqa: F401 +import attr +import requests.certs import voluptuous as vol -from homeassistant.helpers.typing import HomeAssistantType, ConfigType, \ - ServiceDataType -from homeassistant.core import callback, Event, ServiceCall -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass -from homeassistant.helpers import template, config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util.async_ import ( - run_coroutine_threadsafe, run_callback_threadsafe) +from homeassistant import config_entries from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME, - CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) + CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME, + CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import Event, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import template +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ( + ConfigType, HomeAssistantType, ServiceDataType) +from homeassistant.loader import bind_hass +from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util.async_ import ( + run_callback_threadsafe, run_coroutine_threadsafe) +# Loading the config flow file will register the flow +from . import config_flow # noqa # pylint: disable=unused-import +from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY from .server import HBMQTT_CONFIG_SCHEMA -REQUIREMENTS = ['paho-mqtt==1.3.1'] +REQUIREMENTS = ['paho-mqtt==1.4.0'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'mqtt' DATA_MQTT = 'mqtt' +DATA_MQTT_CONFIG = 'mqtt_config' +DATA_MQTT_HASS_CONFIG = 'mqtt_hass_config' SERVICE_PUBLISH = 'publish' CONF_EMBEDDED = 'embedded' -CONF_BROKER = 'broker' + CONF_CLIENT_ID = 'client_id' -CONF_DISCOVERY = 'discovery' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_KEEPALIVE = 'keepalive' CONF_CERTIFICATE = 'certificate' @@ -76,7 +81,6 @@ DEFAULT_KEEPALIVE = 60 DEFAULT_QOS = 0 DEFAULT_RETAIN = False DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_DISCOVERY = False DEFAULT_DISCOVERY_PREFIX = 'homeassistant' DEFAULT_TLS_PROTOCOL = 'auto' DEFAULT_PAYLOAD_AVAILABLE = 'online' @@ -87,6 +91,7 @@ ATTR_PAYLOAD = 'payload' ATTR_PAYLOAD_TEMPLATE = 'payload_template' ATTR_QOS = CONF_QOS ATTR_RETAIN = CONF_RETAIN +ATTR_DISCOVERY_HASH = 'discovery_hash' MAX_RECONNECT_WAIT = 300 # seconds @@ -291,8 +296,7 @@ def subscribe(hass: HomeAssistantType, topic: str, return remove -async def _async_setup_server(hass: HomeAssistantType, - config: ConfigType): +async def _async_setup_server(hass: HomeAssistantType, config: ConfigType): """Try to start embedded MQTT broker. This method is a coroutine. @@ -312,26 +316,27 @@ async def _async_setup_server(hass: HomeAssistantType, if not success: return None + return broker_config -async def _async_setup_discovery(hass: HomeAssistantType, - config: ConfigType) -> bool: +async def _async_setup_discovery(hass: HomeAssistantType, conf: ConfigType, + hass_config: ConfigType, + config_entry) -> bool: """Try to start the discovery of MQTT devices. This method is a coroutine. """ - conf = config.get(DOMAIN, {}) # type: ConfigType - discovery = await async_prepare_setup_platform( - hass, config, DOMAIN, 'discovery') + hass, hass_config, DOMAIN, 'discovery') if discovery is None: _LOGGER.error("Unable to load MQTT discovery") return False success = await discovery.async_start( - hass, conf[CONF_DISCOVERY_PREFIX], config) # type: bool + hass, conf[CONF_DISCOVERY_PREFIX], hass_config, + config_entry) # type: bool return success @@ -340,20 +345,21 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Start the MQTT protocol service.""" conf = config.get(DOMAIN) # type: Optional[ConfigType] + # We need this because discovery can cause components to be set up and + # otherwise it will not load the users config. + # This needs a better solution. + hass.data[DATA_MQTT_HASS_CONFIG] = config + if conf is None: - conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - conf = cast(ConfigType, conf) + # If we have a config entry, setup is done by that config entry. + # If there is no config entry, this should fail. + return bool(hass.config_entries.async_entries(DOMAIN)) - client_id = conf.get(CONF_CLIENT_ID) # type: Optional[str] - keepalive = conf.get(CONF_KEEPALIVE) # type: int + conf = dict(conf) - # Only setup if embedded config passed in or no broker specified - if CONF_EMBEDDED not in conf and CONF_BROKER in conf: - broker_config = None - else: + if CONF_EMBEDDED in conf or CONF_BROKER not in conf: if (conf.get(CONF_PASSWORD) is None and - config.get('http') is not None and - config['http'].get('api_password') is not None): + config.get('http', {}).get('api_password') is not None): _LOGGER.error( "Starting from release 0.76, the embedded MQTT broker does not" " use api_password as default password anymore. Please set" @@ -363,48 +369,91 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: broker_config = await _async_setup_server(hass, config) - if CONF_BROKER in conf: - broker = conf[CONF_BROKER] # type: str - port = conf[CONF_PORT] # type: int - username = conf.get(CONF_USERNAME) # type: Optional[str] - password = conf.get(CONF_PASSWORD) # type: Optional[str] - certificate = conf.get(CONF_CERTIFICATE) # type: Optional[str] - client_key = conf.get(CONF_CLIENT_KEY) # type: Optional[str] - client_cert = conf.get(CONF_CLIENT_CERT) # type: Optional[str] - tls_insecure = conf.get(CONF_TLS_INSECURE) # type: Optional[bool] - protocol = conf[CONF_PROTOCOL] # type: str - elif broker_config is not None: - # If no broker passed in, auto config to internal server - broker, port, username, password, certificate, protocol = broker_config - # Embedded broker doesn't have some ssl variables - client_key, client_cert, tls_insecure = None, None, None - # hbmqtt requires a client id to be set. - if client_id is None: - client_id = 'home-assistant' - else: - err = "Unable to start MQTT broker." - if conf.get(CONF_EMBEDDED) is not None: - # Explicit embedded config, requires explicit broker config - err += " (Broker configuration required.)" - _LOGGER.error(err) + if broker_config is None: + _LOGGER.error("Unable to start embedded MQTT broker") + return False + + conf.update({ + CONF_BROKER: broker_config[0], + CONF_PORT: broker_config[1], + CONF_USERNAME: broker_config[2], + CONF_PASSWORD: broker_config[3], + CONF_CERTIFICATE: broker_config[4], + CONF_PROTOCOL: broker_config[5], + CONF_CLIENT_KEY: None, + CONF_CLIENT_CERT: None, + CONF_TLS_INSECURE: None, + }) + + hass.data[DATA_MQTT_CONFIG] = conf + + # Only import if we haven't before. + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, + data={} + )) + + return True + + +async def async_setup_entry(hass, entry): + """Load a config entry.""" + conf = hass.data.get(DATA_MQTT_CONFIG) + + # Config entry was created because user had configuration.yaml entry + # They removed that, so remove entry. + if conf is None and entry.source == config_entries.SOURCE_IMPORT: + hass.async_create_task( + hass.config_entries.async_remove(entry.entry_id)) return False + # If user didn't have configuration.yaml config, generate defaults + if conf is None: + conf = CONFIG_SCHEMA({ + DOMAIN: entry.data + })[DOMAIN] + elif any(key in conf for key in entry.data): + _LOGGER.warning( + "Data in your config entry is going to override your " + "configuration.yaml: %s", entry.data) + + conf.update(entry.data) + + broker = conf[CONF_BROKER] + port = conf[CONF_PORT] + client_id = conf.get(CONF_CLIENT_ID) + keepalive = conf[CONF_KEEPALIVE] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + client_key = conf.get(CONF_CLIENT_KEY) + client_cert = conf.get(CONF_CLIENT_CERT) + tls_insecure = conf.get(CONF_TLS_INSECURE) + protocol = conf[CONF_PROTOCOL] + # For cloudmqtt.com, secured connection, auto fill in certificate - if (certificate is None and 19999 < port < 30000 and - broker.endswith('.cloudmqtt.com')): - certificate = os.path.join(os.path.dirname(__file__), - 'addtrustexternalcaroot.crt') + if (conf.get(CONF_CERTIFICATE) is None and + 19999 < conf[CONF_PORT] < 30000 and + conf[CONF_BROKER].endswith('.cloudmqtt.com')): + certificate = os.path.join( + os.path.dirname(__file__), 'addtrustexternalcaroot.crt') # When the certificate is set to auto, use bundled certs from requests - if certificate == 'auto': + elif conf.get(CONF_CERTIFICATE) == 'auto': certificate = requests.certs.where() - will_message = None # type: Optional[Message] - if conf.get(CONF_WILL_MESSAGE) is not None: - will_message = Message(**conf.get(CONF_WILL_MESSAGE)) - birth_message = None # type: Optional[Message] - if conf.get(CONF_BIRTH_MESSAGE) is not None: - birth_message = Message(**conf.get(CONF_BIRTH_MESSAGE)) + else: + certificate = None + + if CONF_WILL_MESSAGE in conf: + will_message = Message(**conf[CONF_WILL_MESSAGE]) + else: + will_message = None + + if CONF_BIRTH_MESSAGE in conf: + birth_message = Message(**conf[CONF_BIRTH_MESSAGE]) + else: + birth_message = None # Be able to override versions other than TLSv1.0 under Python3.6 conf_tls_version = conf.get(CONF_TLS_VERSION) # type: str @@ -422,14 +471,27 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: else: tls_version = ssl.PROTOCOL_TLSv1 - try: - hass.data[DATA_MQTT] = MQTT( - hass, broker, port, client_id, keepalive, username, password, - certificate, client_key, client_cert, tls_insecure, protocol, - will_message, birth_message, tls_version) - except socket.error: - _LOGGER.exception("Can't connect to the broker. " - "Please check your settings and the broker itself") + hass.data[DATA_MQTT] = MQTT( + hass, + broker=broker, + port=port, + client_id=client_id, + keepalive=keepalive, + username=username, + password=password, + certificate=certificate, + client_key=client_key, + client_cert=client_cert, + tls_insecure=tls_insecure, + protocol=protocol, + will_message=will_message, + birth_message=birth_message, + tls_version=tls_version, + ) + + success = await hass.data[DATA_MQTT].async_connect() # type: bool + + if not success: return False async def async_stop_mqtt(event: Event): @@ -438,10 +500,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) - success = await hass.data[DATA_MQTT].async_connect() # type: bool - if not success: - return False - async def async_publish_service(call: ServiceCall): """Handle MQTT publish service calls.""" msg_topic = call.data[ATTR_TOPIC] # type: str @@ -468,7 +526,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=MQTT_PUBLISH_SCHEMA) if conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, config) + await _async_setup_discovery( + hass, conf, hass.data[DATA_MQTT_HASS_CONFIG], entry) return True @@ -502,7 +561,8 @@ class MQTT: certificate: Optional[str], client_key: Optional[str], client_cert: Optional[str], tls_insecure: Optional[bool], protocol: Optional[str], will_message: Optional[Message], - birth_message: Optional[Message], tls_version) -> None: + birth_message: Optional[Message], + tls_version: Optional[int]) -> None: """Initialize Home Assistant MQTT client.""" import paho.mqtt.client as mqtt @@ -564,12 +624,12 @@ class MQTT: result = await self.hass.async_add_job( self._mqttc.connect, self.broker, self.port, self.keepalive) except OSError as err: - _LOGGER.error('Failed to connect due to exception: %s', err) + _LOGGER.error("Failed to connect due to exception: %s", err) return False if result != 0: import paho.mqtt.client as mqtt - _LOGGER.error('Failed to connect: %s', mqtt.error_string(result)) + _LOGGER.error("Failed to connect: %s", mqtt.error_string(result)) return False self._mqttc.loop_start() @@ -596,7 +656,7 @@ class MQTT: This method is a coroutine. """ if not isinstance(topic, str): - raise HomeAssistantError("topic needs to be a string!") + raise HomeAssistantError("Topic needs to be a string!") subscription = Subscription(topic, msg_callback, qos, encoding) self.subscriptions.append(subscription) @@ -638,8 +698,8 @@ class MQTT: self._mqttc.subscribe, topic, qos) _raise_on_error(result) - def _mqtt_on_connect(self, _mqttc, _userdata, _flags, - result_code: int) -> None: + def _mqtt_on_connect( + self, _mqttc, _userdata, _flags, result_code: int) -> None: """On connect callback. Resubscribe to all topics we were subscribed to and publish birth @@ -648,7 +708,7 @@ class MQTT: import paho.mqtt.client as mqtt if result_code != mqtt.CONNACK_ACCEPTED: - _LOGGER.error('Unable to connect to the MQTT broker: %s', + _LOGGER.error("Unable to connect to the MQTT broker: %s", mqtt.connack_string(result_code)) self._mqttc.disconnect() return @@ -682,14 +742,13 @@ class MQTT: try: payload = msg.payload.decode(subscription.encoding) except (AttributeError, UnicodeDecodeError): - _LOGGER.warning("Can't decode payload %s on %s " - "with encoding %s", - msg.payload, msg.topic, - subscription.encoding) + _LOGGER.warning( + "Can't decode payload %s on %s with encoding %s", + msg.payload, msg.topic, subscription.encoding) continue - self.hass.async_run_job(subscription.callback, - msg.topic, payload, msg.qos) + self.hass.async_run_job( + subscription.callback, msg.topic, payload, msg.qos) def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: """Disconnected callback.""" @@ -727,23 +786,14 @@ def _raise_on_error(result_code: int) -> None: def _match_topic(subscription: str, topic: str) -> bool: """Test if topic matches subscription.""" - reg_ex_parts = [] # type: List[str] - suffix = "" - if subscription.endswith('#'): - subscription = subscription[:-2] - suffix = "(.*)" - sub_parts = subscription.split('/') - for sub_part in sub_parts: - if sub_part == "+": - reg_ex_parts.append(r"([^\/]+)") - else: - reg_ex_parts.append(re.escape(sub_part)) - - reg_ex = "^" + (r'\/'.join(reg_ex_parts)) + suffix + "$" - - reg = re.compile(reg_ex) - - return reg.match(topic) is not None + from paho.mqtt.matcher import MQTTMatcher + matcher = MQTTMatcher() + matcher[subscription] = True + try: + next(matcher.iter_match(topic)) + return True + except StopIteration: + return False class MqttAvailability(Entity): @@ -760,7 +810,7 @@ class MqttAvailability(Entity): self._payload_not_available = payload_not_available async def async_added_to_hass(self) -> None: - """Subscribe mqtt events. + """Subscribe MQTT events. This method must be run in the event loop and returns a coroutine. """ @@ -785,3 +835,36 @@ class MqttAvailability(Entity): def available(self) -> bool: """Return if the device is available.""" return self._available + + +class MqttDiscoveryUpdate(Entity): + """Mixin used to handle updated discovery message.""" + + def __init__(self, discovery_hash) -> None: + """Initialize the discovery update mixin.""" + self._discovery_hash = discovery_hash + self._remove_signal = None + + async def async_added_to_hass(self) -> None: + """Subscribe to discovery updates.""" + from homeassistant.helpers.dispatcher import async_dispatcher_connect + from homeassistant.components.mqtt.discovery import ( + ALREADY_DISCOVERED, MQTT_DISCOVERY_UPDATED) + + @callback + def discovery_callback(payload): + """Handle discovery update.""" + _LOGGER.info("Got update for entity with hash: %s '%s'", + self._discovery_hash, payload) + if not payload: + # Empty payload: Remove component + _LOGGER.info("Removing component: %s", self.entity_id) + self.hass.async_create_task(self.async_remove()) + del self.hass.data[ALREADY_DISCOVERED][self._discovery_hash] + self._remove_signal() + + if self._discovery_hash: + self._remove_signal = async_dispatcher_connect( + self.hass, + MQTT_DISCOVERY_UPDATED.format(self._discovery_hash), + discovery_callback) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py new file mode 100644 index 00000000000..22072857b03 --- /dev/null +++ b/homeassistant/components/mqtt/config_flow.py @@ -0,0 +1,88 @@ +"""Config flow for MQTT.""" +from collections import OrderedDict +import queue + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME + +from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY + + +@config_entries.HANDLERS.register('mqtt') +class FlowHandler(config_entries.ConfigFlow): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason='single_instance_allowed') + + return await self.async_step_broker() + + async def async_step_broker(self, user_input=None): + """Confirm the setup.""" + errors = {} + + if user_input is not None: + can_connect = await self.hass.async_add_executor_job( + try_connection, user_input[CONF_BROKER], user_input[CONF_PORT], + user_input.get(CONF_USERNAME), user_input.get(CONF_PASSWORD)) + + if can_connect: + return self.async_create_entry( + title=user_input[CONF_BROKER], data=user_input) + + errors['base'] = 'cannot_connect' + + fields = OrderedDict() + fields[vol.Required(CONF_BROKER)] = str + fields[vol.Required(CONF_PORT, default=1883)] = vol.Coerce(int) + fields[vol.Optional(CONF_USERNAME)] = str + fields[vol.Optional(CONF_PASSWORD)] = str + fields[vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY)] = bool + + return self.async_show_form( + step_id='broker', data_schema=vol.Schema(fields), errors=errors) + + async def async_step_import(self, user_input): + """Import a config entry. + + Special type of import, we're not actually going to store any data. + Instead, we're going to rely on the values that are in config file. + """ + if self._async_current_entries(): + return self.async_abort(reason='single_instance_allowed') + + return self.async_create_entry(title='configuration.yaml', data={}) + + +def try_connection(broker, port, username, password): + """Test if we can connect to an MQTT broker.""" + import paho.mqtt.client as mqtt + client = mqtt.Client() + if username and password: + client.username_pw_set(username, password) + + result = queue.Queue(maxsize=1) + + def on_connect(client_, userdata, flags, result_code): + """Handle connection result.""" + result.put(result_code == mqtt.CONNACK_ACCEPTED) + + client.on_connect = on_connect + + client.connect_async(broker, port) + client.loop_start() + + try: + return result.get(timeout=5) + except queue.Empty: + return False + finally: + client.disconnect() + client.loop_stop() diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py new file mode 100644 index 00000000000..3c22001f91c --- /dev/null +++ b/homeassistant/components/mqtt/const.py @@ -0,0 +1,4 @@ +"""Constants used by multiple MQTT modules.""" +CONF_BROKER = 'broker' +CONF_DISCOVERY = 'discovery' +DEFAULT_DISCOVERY = False diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 128c45f1311..6a0b8555ddb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -9,9 +9,11 @@ import logging import re from homeassistant.components import mqtt -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.components.mqtt import CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH from homeassistant.const import CONF_PLATFORM -from homeassistant.components.mqtt import CONF_STATE_TOPIC +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -37,10 +39,25 @@ ALLOWED_PLATFORMS = { 'alarm_control_panel': ['mqtt'], } +CONFIG_ENTRY_PLATFORMS = { + 'binary_sensor': ['mqtt'], + 'camera': ['mqtt'], + 'cover': ['mqtt'], + 'light': ['mqtt'], + 'sensor': ['mqtt'], + 'switch': ['mqtt'], + 'climate': ['mqtt'], + 'alarm_control_panel': ['mqtt'], +} + ALREADY_DISCOVERED = 'mqtt_discovered_components' +CONFIG_ENTRY_IS_SETUP = 'mqtt_config_entry_is_setup' +MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}' +MQTT_DISCOVERY_NEW = 'mqtt_discovery_new_{}_{}' -async def async_start(hass, discovery_topic, hass_config): +async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, + config_entry=None) -> bool: """Initialize of MQTT Discovery.""" async def async_device_message_received(topic, payload, qos): """Process the received message.""" @@ -51,47 +68,66 @@ async def async_start(hass, discovery_topic, hass_config): _prefix_topic, component, node_id, object_id = match.groups() - try: - payload = json.loads(payload) - except ValueError: - _LOGGER.warning("Unable to parse JSON %s: %s", object_id, payload) - return - if component not in SUPPORTED_COMPONENTS: _LOGGER.warning("Component %s is not supported", component) return - payload = dict(payload) - platform = payload.get(CONF_PLATFORM, 'mqtt') - if platform not in ALLOWED_PLATFORMS.get(component, []): - _LOGGER.warning("Platform %s (component %s) is not allowed", - platform, component) - return - - payload[CONF_PLATFORM] = platform - if CONF_STATE_TOPIC not in payload: - payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format( - discovery_topic, component, '%s/' % node_id if node_id else '', - object_id) - - if ALREADY_DISCOVERED not in hass.data: - hass.data[ALREADY_DISCOVERED] = set() - # If present, the node_id will be included in the discovered object id discovery_id = '_'.join((node_id, object_id)) if node_id else object_id + if ALREADY_DISCOVERED not in hass.data: + hass.data[ALREADY_DISCOVERED] = {} + discovery_hash = (component, discovery_id) + if discovery_hash in hass.data[ALREADY_DISCOVERED]: - _LOGGER.info("Component has already been discovered: %s %s", - component, discovery_id) - return + _LOGGER.info( + "Component has already been discovered: %s %s, sending update", + component, discovery_id) + async_dispatcher_send( + hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), payload) + elif payload: + # Add component + try: + payload = json.loads(payload) + except ValueError: + _LOGGER.warning("Unable to parse JSON %s: '%s'", + object_id, payload) + return - hass.data[ALREADY_DISCOVERED].add(discovery_hash) + payload = dict(payload) + platform = payload.get(CONF_PLATFORM, 'mqtt') + if platform not in ALLOWED_PLATFORMS.get(component, []): + _LOGGER.warning("Platform %s (component %s) is not allowed", + platform, component) + return - _LOGGER.info("Found new component: %s %s", component, discovery_id) + payload[CONF_PLATFORM] = platform + if CONF_STATE_TOPIC not in payload: + payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format( + discovery_topic, component, + '%s/' % node_id if node_id else '', object_id) - await async_load_platform( - hass, component, platform, payload, hass_config) + hass.data[ALREADY_DISCOVERED][discovery_hash] = None + payload[ATTR_DISCOVERY_HASH] = discovery_hash + + _LOGGER.info("Found new component: %s %s", component, discovery_id) + + if platform not in CONFIG_ENTRY_PLATFORMS.get(component, []): + await async_load_platform( + hass, component, platform, payload, hass_config) + return + + config_entries_key = '{}.{}'.format(component, platform) + if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + await hass.config_entries.async_forward_entry_setup( + config_entry, component) + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) + + async_dispatcher_send(hass, MQTT_DISCOVERY_NEW.format( + component, platform), payload) + + hass.data[CONFIG_ENTRY_IS_SETUP] = set() await mqtt.async_subscribe( hass, discovery_topic + '/#', async_device_message_received, 0) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 5fc365342ae..dda2214ce46 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,10 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['hbmqtt==0.9.2'] +REQUIREMENTS = ['hbmqtt==0.9.4'] + +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['http'] # None allows custom config to be created through generate_config @@ -27,8 +30,6 @@ HBMQTT_CONFIG_SCHEMA = vol.Any(None, vol.Schema({ }) }, extra=vol.ALLOW_EXTRA)) -_LOGGER = logging.getLogger(__name__) - @asyncio.coroutine def async_start(hass, password, server_config): @@ -85,6 +86,10 @@ def generate_config(hass, passwd, password): 'allow-anonymous': password is None }, 'plugins': ['auth_anonymous'], + 'topic-check': { + 'enabled': True, + 'plugins': ['topic_taboo'], + }, } if password: diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json new file mode 100644 index 00000000000..0a2cb255cc4 --- /dev/null +++ b/homeassistant/components/mqtt/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "MQTT", + "step": { + "broker": { + "title": "MQTT", + "description": "Please enter the connection information of your MQTT broker.", + "data": { + "broker": "Broker", + "port": "Port", + "username": "Username", + "password": "Password", + "discovery": "Enable discovery" + } + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of MQTT is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to the broker." + } + } +} diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 25da38e7f75..38af84e3176 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,7 +17,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pybotvac==0.0.9'] +REQUIREMENTS = ['pybotvac==0.0.10'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json index 2fb17916aee..e15d0106da8 100644 --- a/homeassistant/components/nest/.translations/ca.json +++ b/homeassistant/components/nest/.translations/ca.json @@ -9,7 +9,7 @@ "error": { "internal_error": "Error intern al validar el codi", "invalid_code": "Codi inv\u00e0lid", - "timeout": "Temps d'espera de validaci\u00f3 del codi esgotat", + "timeout": "S'ha acabat el temps d'espera durant la validaci\u00f3 del codi.", "unknown": "Error desconegut al validar el codi" }, "step": { diff --git a/homeassistant/components/nest/.translations/da.json b/homeassistant/components/nest/.translations/da.json index 4410f83d2ca..5edf3a00af4 100644 --- a/homeassistant/components/nest/.translations/da.json +++ b/homeassistant/components/nest/.translations/da.json @@ -16,8 +16,10 @@ "link": { "data": { "code": "PIN-kode" - } + }, + "title": "Link Nest-konto" } - } + }, + "title": "Nest" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json index 86b50ab3c10..500862039a2 100644 --- a/homeassistant/components/nest/.translations/de.json +++ b/homeassistant/components/nest/.translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_setup": "Sie k\u00f6nnen nur ein einziges Nest-Konto konfigurieren.", + "already_setup": "Du kannst nur ein einziges Nest-Konto konfigurieren.", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL", - "no_flows": "Sie m\u00fcssen Nest konfigurieren, bevor Sie sich authentifizieren k\u00f6nnen. [Bitte lesen Sie die Anweisungen] (https://www.home-assistant.io/components/nest/)." + "no_flows": "Du musst Nest konfigurieren, bevor du dich authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Ein interner Fehler ist aufgetreten", @@ -17,14 +17,14 @@ "data": { "flow_impl": "Anbieter" }, - "description": "W\u00e4hlen Sie, \u00fcber welchen Authentifizierungsanbieter Sie sich bei Nest authentifizieren m\u00f6chten.", + "description": "W\u00e4hlen, \u00fcber welchen Authentifizierungsanbieter du dich bei Nest authentifizieren m\u00f6chtest.", "title": "Authentifizierungsanbieter" }, "link": { "data": { "code": "PIN Code" }, - "description": "[Autorisieren Sie ihr Konto] ( {url} ), um ihren Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcgen Sie anschlie\u00dfend den erhaltenen PIN Code hier ein.", + "description": "[Autorisiere dein Konto] ( {url} ), um deinen Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcge anschlie\u00dfend den erhaltenen PIN Code hier ein.", "title": "Nest-Konto verkn\u00fcpfen" } }, diff --git a/homeassistant/components/nest/.translations/fr.json b/homeassistant/components/nest/.translations/fr.json index 62a4d7deec9..822ec6ae836 100644 --- a/homeassistant/components/nest/.translations/fr.json +++ b/homeassistant/components/nest/.translations/fr.json @@ -1,7 +1,33 @@ { "config": { "abort": { - "already_setup": "Vous ne pouvez configurer qu'un seul compte Nest." - } + "already_setup": "Vous ne pouvez configurer qu'un seul compte Nest.", + "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "no_flows": "Vous devez configurer Nest avant de pouvoir vous authentifier avec celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Erreur interne lors de la validation du code", + "invalid_code": "Code invalide", + "timeout": "D\u00e9lai de la validation du code expir\u00e9", + "unknown": "Erreur inconnue lors de la validation du code" + }, + "step": { + "init": { + "data": { + "flow_impl": "Fournisseur" + }, + "description": "S\u00e9lectionnez via quel fournisseur d'authentification vous souhaitez vous authentifier avec Nest.", + "title": "Fournisseur d'authentification" + }, + "link": { + "data": { + "code": "Code PIN" + }, + "description": "Pour associer votre compte Nest, [autorisez votre compte] ( {url} ). \n\n Apr\u00e8s autorisation, copiez-collez le code PIN fourni ci-dessous.", + "title": "Lier un compte Nest" + } + }, + "title": "Nest" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/id.json b/homeassistant/components/nest/.translations/id.json new file mode 100644 index 00000000000..58f86f5474e --- /dev/null +++ b/homeassistant/components/nest/.translations/id.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Anda hanya dapat mengonfigurasi satu akun Nest.", + "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", + "authorize_url_timeout": "Waktu tunggu menghasilkan otorisasi url telah habis.", + "no_flows": "Anda harus mengonfigurasi Nest sebelum dapat mengautentikasi dengan Nest. [Silakan baca instruksi] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Kesalahan Internal memvalidasi kode", + "invalid_code": "Kode salah", + "timeout": "Waktu tunggu memvalidasi kode telah habis.", + "unknown": "Error tidak diketahui saat memvalidasi kode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Penyedia" + }, + "description": "Pilih melalui penyedia autentikasi mana yang ingin Anda autentikasi dengan Nest.", + "title": "Penyedia Otentikasi" + }, + "link": { + "data": { + "code": "Kode PIN" + }, + "description": "Untuk menautkan akun Nest Anda, [beri kuasa akun Anda] ( {url} ). \n\n Setelah otorisasi, salin-tempel kode pin yang disediakan di bawah ini.", + "title": "Hubungkan Akun Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/nn.json b/homeassistant/components/nest/.translations/nn.json new file mode 100644 index 00000000000..be3915c464f --- /dev/null +++ b/homeassistant/components/nest/.translations/nn.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan berre konfiguere \u00e9in Nest-brukar.", + "authorize_url_fail": "Ukjent feil ved generering av autentiserings-URL", + "authorize_url_timeout": "Tida gjekk ut for generert autentikasjons-URL", + "no_flows": "Du m\u00e5 konfiguere Nest f\u00f8r du kan autentisere den. (Les instruksjonane) (https://www.home-assistant.io/components/nest/)" + }, + "error": { + "internal_error": "Intern feil ved validering av kode", + "invalid_code": "Ugyldig kode", + "timeout": "Tida gjekk ut for validering av kode", + "unknown": "Det hende ein ukjent feil ved validering av kode." + }, + "step": { + "init": { + "data": { + "flow_impl": "Leverand\u00f8r" + }, + "description": "Vel kva for ein autentiseringsleverand\u00f8r du vil godkjenne med Nest.", + "title": "Autentiseringsleverand\u00f8r" + }, + "link": { + "data": { + "code": "Pinkode" + }, + "description": "For \u00e5 linke Nestkontoen din, [autoriser kontoen din]{url}.\nEtter autentiseringa, kopier-lim inn koda du fekk under her.", + "title": "Link Nestkonto" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 57111350396..f609c774b12 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -309,6 +309,37 @@ class NestSensorDevice(Entity): """Do not need poll thanks using Nest streaming API.""" return False + @property + def unique_id(self): + """Return unique id based on device serial and variable.""" + return "{}-{}".format(self.device.serial, self.variable) + + @property + def device_info(self): + """Return information about the device.""" + if not hasattr(self.device, 'name_long'): + name = self.structure.name + model = "Structure" + else: + name = self.device.name_long + if self.device.is_thermostat: + model = 'Thermostat' + elif self.device.is_camera: + model = 'Camera' + elif self.device.is_smoke_co_alarm: + model = 'Nest Protect' + else: + model = None + + return { + 'identifiers': { + (DOMAIN, self.device.serial) + }, + 'name': name, + 'manufacturer': 'Nest Labs', + 'model': model, + } + def update(self): """Do not use NestSensorDevice directly.""" raise NotImplementedError diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index c9987693b1a..3385fd4f850 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -7,7 +7,7 @@ import os import async_timeout import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.util.json import load_json @@ -49,10 +49,11 @@ class CodeInvalid(NestAuthError): @config_entries.HANDLERS.register(DOMAIN) -class NestFlowHandler(data_entry_flow.FlowHandler): +class NestFlowHandler(config_entries.ConfigFlow): """Handle a Nest config flow.""" VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH def __init__(self): """Initialize the Nest config flow.""" diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 5ab10cc2a5e..393a36e4a9c 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -11,7 +11,8 @@ from .const import DOMAIN def initialize(hass, client_id, client_secret): """Initialize a local auth provider.""" config_flow.register_flow_implementation( - hass, DOMAIN, 'local', partial(generate_auth_url, client_id), + hass, DOMAIN, 'configuration.yaml', + partial(generate_auth_url, client_id), partial(resolve_auth_code, hass, client_id, client_secret) ) diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index c25b57fbd62..d8924c6c301 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyatmo==1.1.1'] +REQUIREMENTS = ['pyatmo==1.2'] _LOGGER = logging.getLogger(__name__) @@ -101,10 +101,10 @@ class CameraData: return self.module_names def get_camera_type(self, camera=None, home=None, cid=None): - """Return all module available on the API as a list.""" - for camera_name in self.camera_names: - self.camera_type = self.camera_data.cameraType(camera_name) - return self.camera_type + """Return camera type for a camera, cid has preference over camera.""" + self.camera_type = self.camera_data.cameraType(camera=camera, + home=home, cid=cid) + return self.camera_type @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py index 7f54e6fd6f9..e5e9a0fc2e9 100644 --- a/homeassistant/components/netgear_lte.py +++ b/homeassistant/components/netgear_lte.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/netgear_lte/ """ import asyncio from datetime import timedelta +import logging import voluptuous as vol import attr @@ -17,7 +18,9 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.util import Throttle -REQUIREMENTS = ['eternalegypt==0.0.3'] +REQUIREMENTS = ['eternalegypt==0.0.5'] + +_LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -37,17 +40,23 @@ class ModemData: """Class for modem state.""" modem = attr.ib() - serial_number = attr.ib(init=False) - unread_count = attr.ib(init=False) - usage = attr.ib(init=False) + + serial_number = attr.ib(init=False, default=None) + unread_count = attr.ib(init=False, default=None) + usage = attr.ib(init=False, default=None) @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Call the API to update the data.""" - information = await self.modem.information() - self.serial_number = information.serial_number - self.unread_count = sum(1 for x in information.sms if x.unread) - self.usage = information.usage + import eternalegypt + try: + information = await self.modem.information() + self.serial_number = information.serial_number + self.unread_count = sum(1 for x in information.sms if x.unread) + self.usage = information.usage + except eternalegypt.Error: + self.unread_count = None + self.usage = None @attr.s @@ -81,17 +90,27 @@ async def async_setup(hass, config): return True -async def _setup_lte(hass, lte_config): +async def _setup_lte(hass, lte_config, delay=0): """Set up a Netgear LTE modem.""" import eternalegypt + if delay: + await asyncio.sleep(delay) + host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] websession = hass.data[DATA_KEY].websession modem = eternalegypt.Modem(hostname=host, websession=websession) - await modem.login(password=password) + + try: + await modem.login(password=password) + except eternalegypt.Error: + delay = max(15, min(2*delay, 300)) + _LOGGER.warning("Retrying %s in %d seconds", host, delay) + hass.loop.create_task(_setup_lte(hass, lte_config, delay)) + return modem_data = ModemData(modem) await modem_data.async_update() diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 4de35d3f850..0535d7caa6d 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.setup import async_prepare_setup_platform from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_per_platform, discovery @@ -50,22 +49,6 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def send_message(hass, message, title=None, data=None): - """Send a notification message.""" - info = { - ATTR_MESSAGE: message - } - - if title is not None: - info[ATTR_TITLE] = title - - if data is not None: - info[ATTR_DATA] = data - - hass.services.call(DOMAIN, SERVICE_NOTIFY, info) - - @asyncio.coroutine def async_setup(hass, config): """Set up the notify services.""" diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py index dca47a46dbf..0cf4bced360 100644 --- a/homeassistant/components/notify/discord.py +++ b/homeassistant/components/notify/discord.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - PLATFORM_SCHEMA, BaseNotificationService, ATTR_TARGET) + PLATFORM_SCHEMA, BaseNotificationService, ATTR_TARGET, ATTR_DATA) from homeassistant.const import CONF_TOKEN _LOGGER = logging.getLogger(__name__) @@ -22,6 +22,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): cv.string }) +ATTR_IMAGES = 'images' + def get_service(hass, config, discovery_info=None): """Get the Discord notification service.""" @@ -53,9 +55,15 @@ class DiscordNotificationService(BaseNotificationService): def on_ready(): """Send the messages when the bot is ready.""" try: + data = kwargs.get(ATTR_DATA) + if data: + images = data.get(ATTR_IMAGES) for channelid in kwargs[ATTR_TARGET]: channel = discord.Object(id=channelid) yield from discord_bot.send_message(channel, message) + if images: + for anum, f_name in enumerate(images): + yield from discord_bot.send_file(channel, f_name) except (discord.errors.HTTPException, discord.errors.NotFound) as error: _LOGGER.warning("Communication error: %s", error) diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/notify/ios.py index 8609e1dabee..e6a37d707ad 100644 --- a/homeassistant/components/notify/ios.py +++ b/homeassistant/components/notify/ios.py @@ -24,7 +24,7 @@ DEPENDENCIES = ["ios"] # pylint: disable=invalid-name -def log_rate_limits(target, resp, level=20): +def log_rate_limits(hass, target, resp, level=20): """Output rate limit log line at given level.""" rate_limits = resp["rateLimits"] resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"]) @@ -33,7 +33,7 @@ def log_rate_limits(target, resp, level=20): "%d sent, %d allowed, %d errors, " "resets in %s") _LOGGER.log(level, rate_limit_msg, - ios.device_name_for_push_id(target), + ios.device_name_for_push_id(hass, target), rate_limits["successful"], rate_limits["maximum"], rate_limits["errors"], str(resetsAtTime).split(".")[0]) @@ -45,7 +45,7 @@ def get_service(hass, config, discovery_info=None): # Need this to enable requirements checking in the app. hass.config.components.add("notify.ios") - if not ios.devices_with_push(): + if not ios.devices_with_push(hass): _LOGGER.error("The notify.ios platform was loaded but no " "devices exist! Please check the documentation at " "https://home-assistant.io/ecosystem/ios/notifications" @@ -64,7 +64,7 @@ class iOSNotificationService(BaseNotificationService): @property def targets(self): """Return a dictionary of registered targets.""" - return ios.devices_with_push() + return ios.devices_with_push(self.hass) def send_message(self, message="", **kwargs): """Send a message to the Lambda APNS gateway.""" @@ -78,13 +78,13 @@ class iOSNotificationService(BaseNotificationService): targets = kwargs.get(ATTR_TARGET) if not targets: - targets = ios.enabled_push_ids() + targets = ios.enabled_push_ids(self.hass) if kwargs.get(ATTR_DATA) is not None: data[ATTR_DATA] = kwargs.get(ATTR_DATA) for target in targets: - if target not in ios.enabled_push_ids(): + if target not in ios.enabled_push_ids(self.hass): _LOGGER.error("The target (%s) does not exist in .ios.conf.", targets) return @@ -102,8 +102,8 @@ class iOSNotificationService(BaseNotificationService): message = req.json().get("message", fallback_message) if req.status_code == 429: _LOGGER.warning(message) - log_rate_limits(target, req.json(), 30) + log_rate_limits(self.hass, target, req.json(), 30) else: _LOGGER.error(message) else: - log_rate_limits(target, req.json()) + log_rate_limits(self.hass, target, req.json()) diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/notify/netgear_lte.py index 97dfe504a51..9ba804e193d 100644 --- a/homeassistant/components/notify/netgear_lte.py +++ b/homeassistant/components/notify/netgear_lte.py @@ -4,6 +4,8 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.netgear_lte/ """ +import logging + import voluptuous as vol import attr @@ -17,6 +19,8 @@ from ..netgear_lte import DATA_KEY DEPENDENCIES = ['netgear_lte'] +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), @@ -25,21 +29,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_get_service(hass, config, discovery_info=None): """Get the notification service.""" - modem_data = hass.data[DATA_KEY].get_modem_data(config) - phone = config.get(ATTR_TARGET) - return NetgearNotifyService(modem_data, phone) + return NetgearNotifyService(hass, config) @attr.s class NetgearNotifyService(BaseNotificationService): """Implementation of a notification service.""" - modem_data = attr.ib() - phone = attr.ib() + hass = attr.ib() + config = attr.ib() async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" - targets = kwargs.get(ATTR_TARGET, self.phone) + modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) + if not modem_data: + _LOGGER.error("No modem available") + return + + phone = self.config.get(ATTR_TARGET) + targets = kwargs.get(ATTR_TARGET, phone) if targets and message: for target in targets: - await self.modem_data.modem.sms(target, message) + import eternalegypt + try: + await modem_data.modem.sms(target, message) + except eternalegypt.Error: + _LOGGER.error("Unable to send to %s", target) diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py index 044a037cc29..faf5e90e016 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -9,6 +9,8 @@ import io import base64 import requests +from requests.auth import HTTPBasicAuth +from requests.auth import HTTPDigestAuth import voluptuous as vol from homeassistant.components.notify import ( @@ -21,12 +23,14 @@ _LOGGER = logging.getLogger(__name__) CONF_IP = 'host' CONF_DURATION = 'duration' +CONF_FONTSIZE = 'fontsize' CONF_POSITION = 'position' CONF_TRANSPARENCY = 'transparency' CONF_COLOR = 'color' CONF_INTERRUPT = 'interrupt' DEFAULT_DURATION = 5 +DEFAULT_FONTSIZE = 'medium' DEFAULT_POSITION = 'bottom-right' DEFAULT_TRANSPARENCY = 'default' DEFAULT_COLOR = 'grey' @@ -37,11 +41,29 @@ DEFAULT_ICON = ( 'cMXEAAAAASUVORK5CYII=') ATTR_DURATION = 'duration' +ATTR_FONTSIZE = 'fontsize' ATTR_POSITION = 'position' ATTR_TRANSPARENCY = 'transparency' ATTR_COLOR = 'color' ATTR_BKGCOLOR = 'bkgcolor' ATTR_INTERRUPT = 'interrupt' +ATTR_IMAGE = 'filename2' +ATTR_FILE = 'file' +# Attributes contained in file +ATTR_FILE_URL = 'url' +ATTR_FILE_PATH = 'path' +ATTR_FILE_USERNAME = 'username' +ATTR_FILE_PASSWORD = 'password' +ATTR_FILE_AUTH = 'auth' +# Any other value or absence of 'auth' lead to basic authentication being used +ATTR_FILE_AUTH_DIGEST = 'digest' + +FONTSIZES = { + 'small': 1, + 'medium': 0, + 'large': 2, + 'max': 3 +} POSITIONS = { 'bottom-right': 0, @@ -75,6 +97,8 @@ COLORS = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_IP): cv.string, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Coerce(int), + vol.Optional(CONF_FONTSIZE, default=DEFAULT_FONTSIZE): + vol.In(FONTSIZES.keys()), vol.Optional(CONF_POSITION, default=DEFAULT_POSITION): vol.In(POSITIONS.keys()), vol.Optional(CONF_TRANSPARENCY, default=DEFAULT_TRANSPARENCY): @@ -90,6 +114,7 @@ def get_service(hass, config, discovery_info=None): """Get the Notifications for Android TV notification service.""" remoteip = config.get(CONF_IP) duration = config.get(CONF_DURATION) + fontsize = config.get(CONF_FONTSIZE) position = config.get(CONF_POSITION) transparency = config.get(CONF_TRANSPARENCY) color = config.get(CONF_COLOR) @@ -97,23 +122,26 @@ def get_service(hass, config, discovery_info=None): timeout = config.get(CONF_TIMEOUT) return NFAndroidTVNotificationService( - remoteip, duration, position, transparency, color, interrupt, timeout) + remoteip, duration, fontsize, position, + transparency, color, interrupt, timeout, hass.config.is_allowed_path) class NFAndroidTVNotificationService(BaseNotificationService): """Notification service for Notifications for Android TV.""" - def __init__(self, remoteip, duration, position, transparency, color, - interrupt, timeout): + def __init__(self, remoteip, duration, fontsize, position, transparency, + color, interrupt, timeout, is_allowed_path): """Initialize the service.""" self._target = 'http://{}:7676'.format(remoteip) self._default_duration = duration + self._default_fontsize = fontsize self._default_position = position self._default_transparency = transparency self._default_color = color self._default_interrupt = interrupt self._timeout = timeout self._icon_file = io.BytesIO(base64.b64decode(DEFAULT_ICON)) + self.is_allowed_path = is_allowed_path def send_message(self, message="", **kwargs): """Send a message to a Android TV device.""" @@ -123,7 +151,8 @@ class NFAndroidTVNotificationService(BaseNotificationService): 'application/octet-stream', {'Expires': '0'}), type='0', title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - msg=message, duration="%i" % self._default_duration, + msg=message, duration='%i' % self._default_duration, + fontsize='%i' % FONTSIZES.get(self._default_fontsize), position='%i' % POSITIONS.get(self._default_position), bkgcolor='%s' % COLORS.get(self._default_color), transparency='%i' % TRANSPARENCIES.get( @@ -140,6 +169,13 @@ class NFAndroidTVNotificationService(BaseNotificationService): except ValueError: _LOGGER.warning("Invalid duration-value: %s", str(duration)) + if ATTR_FONTSIZE in data: + fontsize = data.get(ATTR_FONTSIZE) + if fontsize in FONTSIZES: + payload[ATTR_FONTSIZE] = '%i' % FONTSIZES.get(fontsize) + else: + _LOGGER.warning("Invalid fontsize-value: %s", + str(fontsize)) if ATTR_POSITION in data: position = data.get(ATTR_POSITION) if position in POSITIONS: @@ -168,6 +204,19 @@ class NFAndroidTVNotificationService(BaseNotificationService): except vol.Invalid: _LOGGER.warning("Invalid interrupt-value: %s", str(interrupt)) + filedata = data.get(ATTR_FILE) if data else None + if filedata is not None: + # Load from file or URL + file_as_bytes = self.load_file( + url=filedata.get(ATTR_FILE_URL), + local_path=filedata.get(ATTR_FILE_PATH), + username=filedata.get(ATTR_FILE_USERNAME), + password=filedata.get(ATTR_FILE_PASSWORD), + auth=filedata.get(ATTR_FILE_AUTH)) + if file_as_bytes: + payload[ATTR_IMAGE] = ( + 'image', file_as_bytes, + 'application/octet-stream', {'Expires': '0'}) try: _LOGGER.debug("Payload: %s", str(payload)) @@ -178,3 +227,37 @@ class NFAndroidTVNotificationService(BaseNotificationService): except requests.exceptions.ConnectionError as err: _LOGGER.error("Error communicating with %s: %s", self._target, str(err)) + + def load_file(self, url=None, local_path=None, username=None, + password=None, auth=None): + """Load image/document/etc from a local path or URL.""" + try: + if url is not None: + # Check whether authentication parameters are provided + if username is not None and password is not None: + # Use digest or basic authentication + if ATTR_FILE_AUTH_DIGEST == auth: + auth_ = HTTPDigestAuth(username, password) + else: + auth_ = HTTPBasicAuth(username, password) + # Load file from URL with authentication + req = requests.get( + url, auth=auth_, timeout=DEFAULT_TIMEOUT) + else: + # Load file from URL without authentication + req = requests.get(url, timeout=DEFAULT_TIMEOUT) + return req.content + + elif local_path is not None: + # Check whether path is whitelisted in configuration.yaml + if self.is_allowed_path(local_path): + return open(local_path, "rb") + _LOGGER.warning("'%s' is not secure to load data from!", + local_path) + else: + _LOGGER.warning("Neither URL nor local path found in params!") + + except OSError as error: + _LOGGER.error("Can't load from url or local path: %s", error) + + return None diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index d4c5a196a3f..d576cdcc95e 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -136,9 +136,9 @@ class SlackNotificationService(BaseNotificationService): password=None, auth=None): """Load image/document/etc from a local path or URL.""" try: - if url is not None: + if url: # Check whether authentication parameters are provided - if username is not None and password is not None: + if username: # Use digest or basic authentication if ATTR_FILE_AUTH_DIGEST == auth: auth_ = HTTPDigestAuth(username, password) @@ -151,7 +151,7 @@ class SlackNotificationService(BaseNotificationService): req = requests.get(url, timeout=CONF_TIMEOUT) return req.content - elif local_path is not None: + elif local_path: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): return open(local_path, "rb") diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 52d18b9a870..376575e3440 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -23,7 +23,8 @@ def async_is_onboarded(hass): async def async_setup(hass, config): """Set up the onboarding component.""" - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) data = await store.async_load() if data is None: diff --git a/homeassistant/components/openuv/.translations/ar.json b/homeassistant/components/openuv/.translations/ar.json new file mode 100644 index 00000000000..288fae919dc --- /dev/null +++ b/homeassistant/components/openuv/.translations/ar.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ca.json b/homeassistant/components/openuv/.translations/ca.json new file mode 100644 index 00000000000..4a6cf526921 --- /dev/null +++ b/homeassistant/components/openuv/.translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Les coordenades ja estan registrades", + "invalid_api_key": "Contrasenya API no v\u00e0lida" + }, + "step": { + "user": { + "data": { + "api_key": "Contrasenya API d'OpenUV", + "elevation": "Elevaci\u00f3", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "title": "Introdu\u00efu la vostra informaci\u00f3" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/da.json b/homeassistant/components/openuv/.translations/da.json new file mode 100644 index 00000000000..5cda5c6e663 --- /dev/null +++ b/homeassistant/components/openuv/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "OpenUV API N\u00f8gle", + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad" + }, + "title": "Udfyld dine oplysninger" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/de.json b/homeassistant/components/openuv/.translations/de.json new file mode 100644 index 00000000000..7f8121dd96b --- /dev/null +++ b/homeassistant/components/openuv/.translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinaten existieren bereits", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-Schl\u00fcssel", + "elevation": "H\u00f6he", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "title": "Gebe deine Informationen ein" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/en.json b/homeassistant/components/openuv/.translations/en.json new file mode 100644 index 00000000000..df0232d01fc --- /dev/null +++ b/homeassistant/components/openuv/.translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Coordinates already registered", + "invalid_api_key": "Invalid API key" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API Key", + "elevation": "Elevation", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "Fill in your information" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/es-419.json b/homeassistant/components/openuv/.translations/es-419.json new file mode 100644 index 00000000000..6b391c20a0a --- /dev/null +++ b/homeassistant/components/openuv/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Coordenadas ya registradas", + "invalid_api_key": "Clave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "elevation": "Elevaci\u00f3n", + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/fa.json b/homeassistant/components/openuv/.translations/fa.json new file mode 100644 index 00000000000..288fae919dc --- /dev/null +++ b/homeassistant/components/openuv/.translations/fa.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/fr.json b/homeassistant/components/openuv/.translations/fr.json new file mode 100644 index 00000000000..2f83fa30085 --- /dev/null +++ b/homeassistant/components/openuv/.translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Coordonn\u00e9es d\u00e9j\u00e0 enregistr\u00e9es", + "invalid_api_key": "Cl\u00e9 d'API invalide" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API OpenUV", + "elevation": "Altitude", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "Veuillez saisir vos informations" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/he.json b/homeassistant/components/openuv/.translations/he.json new file mode 100644 index 00000000000..262a3d732a2 --- /dev/null +++ b/homeassistant/components/openuv/.translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "\u05d4\u05e7\u05d5\u05d0\u05d5\u05e8\u05d3\u05d9\u05e0\u05d8\u05d5\u05ea \u05db\u05d1\u05e8 \u05e8\u05e9\u05d5\u05de\u05d5\u05ea", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API \u05e9\u05dc OpenUV", + "elevation": "\u05d2\u05d5\u05d1\u05d4 \u05de\u05e2\u05dc \u05e4\u05e0\u05d9 \u05d4\u05d9\u05dd", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + }, + "title": "\u05de\u05dc\u05d0 \u05d0\u05ea \u05d4\u05e4\u05e8\u05d8\u05d9\u05dd \u05e9\u05dc\u05da" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/hu.json b/homeassistant/components/openuv/.translations/hu.json new file mode 100644 index 00000000000..fd30f83c5f8 --- /dev/null +++ b/homeassistant/components/openuv/.translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "A koordin\u00e1t\u00e1k m\u00e1r regisztr\u00e1lva vannak", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API kulcs", + "elevation": "Magass\u00e1g", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + }, + "title": "T\u00f6ltsd ki az adataid" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/id.json b/homeassistant/components/openuv/.translations/id.json new file mode 100644 index 00000000000..beb7c839eb9 --- /dev/null +++ b/homeassistant/components/openuv/.translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinat sudah terdaftar", + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API OpenUV", + "elevation": "Ketinggian", + "latitude": "Lintang", + "longitude": "Garis bujur" + }, + "title": "Isi informasi Anda" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/it.json b/homeassistant/components/openuv/.translations/it.json new file mode 100644 index 00000000000..a18d36693d5 --- /dev/null +++ b/homeassistant/components/openuv/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_api_key": "Chiave API non valida" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ko.json b/homeassistant/components/openuv/.translations/ko.json new file mode 100644 index 00000000000..bb054f0b3a6 --- /dev/null +++ b/homeassistant/components/openuv/.translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "\uc798\ubabb\ub41c API \ud0a4" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API \ud0a4", + "elevation": "\uace0\ub3c4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4" + }, + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/lb.json b/homeassistant/components/openuv/.translations/lb.json new file mode 100644 index 00000000000..86e558cc807 --- /dev/null +++ b/homeassistant/components/openuv/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinate si scho\u00a0registr\u00e9iert", + "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API Schl\u00ebssel", + "elevation": "H\u00e9icht", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad" + }, + "title": "F\u00ebllt \u00e4r Informatiounen aus" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/nl.json b/homeassistant/components/openuv/.translations/nl.json new file mode 100644 index 00000000000..e2b264182d0 --- /dev/null +++ b/homeassistant/components/openuv/.translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Co\u00f6rdinaten al geregistreerd", + "invalid_api_key": "Ongeldige API-sleutel" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-Sleutel", + "elevation": "Hoogte", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + }, + "title": "Vul uw gegevens in" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/nn.json b/homeassistant/components/openuv/.translations/nn.json new file mode 100644 index 00000000000..135e26cede3 --- /dev/null +++ b/homeassistant/components/openuv/.translations/nn.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinata er allereie registrerte", + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-n\u00f8kkel", + "elevation": "H\u00f8gde", + "latitude": "Breiddegrad", + "longitude": "Lengdegrad" + }, + "title": "Fyll ut informasjonen din" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/no.json b/homeassistant/components/openuv/.translations/no.json new file mode 100644 index 00000000000..2ffd5e7fb41 --- /dev/null +++ b/homeassistant/components/openuv/.translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinatene er allerede registrert", + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-n\u00f8kkel", + "elevation": "Elevasjon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + }, + "title": "Fyll ut informasjonen din" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json new file mode 100644 index 00000000000..f6c52ffd04e --- /dev/null +++ b/homeassistant/components/openuv/.translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane", + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API OpenUV", + "elevation": "Wysoko\u015b\u0107", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" + }, + "title": "Wpisz swoje informacje" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/pt-BR.json b/homeassistant/components/openuv/.translations/pt-BR.json new file mode 100644 index 00000000000..905fdbacab8 --- /dev/null +++ b/homeassistant/components/openuv/.translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Coordenadas j\u00e1 cadastradas", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave de API do OpenUV", + "elevation": "Eleva\u00e7\u00e3o", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "Preencha suas informa\u00e7\u00f5es" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/pt.json b/homeassistant/components/openuv/.translations/pt.json new file mode 100644 index 00000000000..36f875efc00 --- /dev/null +++ b/homeassistant/components/openuv/.translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Coordenadas j\u00e1 registadas", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave de API do OpenUV", + "elevation": "Eleva\u00e7\u00e3o", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "Preencha com as suas informa\u00e7\u00f5es" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json new file mode 100644 index 00000000000..bd7fc3f8191 --- /dev/null +++ b/homeassistant/components/openuv/.translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API OpenUV", + "elevation": "\u0412\u044b\u0441\u043e\u0442\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" + }, + "title": "\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0432\u043e\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/sl.json b/homeassistant/components/openuv/.translations/sl.json new file mode 100644 index 00000000000..6d8c537d6aa --- /dev/null +++ b/homeassistant/components/openuv/.translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinate \u017ee registrirane", + "invalid_api_key": "Neveljaven API klju\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API Klju\u010d", + "elevation": "Nadmorska vi\u0161ina", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina" + }, + "title": "Izpolnite svoje podatke" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/sv.json b/homeassistant/components/openuv/.translations/sv.json new file mode 100644 index 00000000000..d9de0f7c0a6 --- /dev/null +++ b/homeassistant/components/openuv/.translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinater \u00e4r redan registrerade", + "invalid_api_key": "Ogiltigt API-l\u00f6senord" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-nyckel", + "elevation": "H\u00f6jd", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "title": "Fyll i dina uppgifter" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/zh-Hans.json b/homeassistant/components/openuv/.translations/zh-Hans.json new file mode 100644 index 00000000000..d8f46d6afe4 --- /dev/null +++ b/homeassistant/components/openuv/.translations/zh-Hans.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "\u5750\u6807\u5df2\u7ecf\u6ce8\u518c", + "invalid_api_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API \u5bc6\u94a5", + "elevation": "\u6d77\u62d4", + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6" + }, + "title": "\u586b\u5199\u60a8\u7684\u4fe1\u606f" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/zh-Hant.json b/homeassistant/components/openuv/.translations/zh-Hant.json new file mode 100644 index 00000000000..2310af22fa2 --- /dev/null +++ b/homeassistant/components/openuv/.translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "identifier_exists": "\u8a72\u5ea7\u6a19\u5df2\u8a3b\u518a", + "invalid_api_key": "API \u5bc6\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API \u5bc6\u9470", + "elevation": "\u6d77\u62d4", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6" + }, + "title": "\u586b\u5beb\u8cc7\u8a0a" + } + }, + "title": "OpenUV" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv.py b/homeassistant/components/openuv/__init__.py similarity index 57% rename from homeassistant/components/openuv.py rename to homeassistant/components/openuv/__init__.py index d696f0e5100..35ab16b4d1f 100644 --- a/homeassistant/components/openuv.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,5 +1,5 @@ """ -Support for data from openuv.io. +Support for UV data from openuv.io. For more details about this component, please refer to the documentation at https://home-assistant.io/components/openuv/ @@ -9,21 +9,24 @@ from datetime import timedelta import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_BINARY_SENSORS, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, CONF_SENSORS) -from homeassistant.helpers import ( - aiohttp_client, config_validation as cv, discovery) +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['pyopenuv==1.0.1'] +from .config_flow import configured_instances +from .const import DOMAIN + +REQUIREMENTS = ['pyopenuv==1.0.4'] _LOGGER = logging.getLogger(__name__) -DOMAIN = 'openuv' - +DATA_OPENUV_CLIENT = 'data_client' +DATA_OPENUV_LISTENER = 'data_listener' DATA_PROTECTION_WINDOW = 'protection_window' DATA_UV = 'uv' @@ -82,39 +85,77 @@ SENSOR_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): - cv.time_period, - }) + DOMAIN: + vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_ELEVATION): float, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + }) }, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): """Set up the OpenUV component.""" - from pyopenuv import Client - from pyopenuv.errors import OpenUvError + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_OPENUV_CLIENT] = {} + hass.data[DOMAIN][DATA_OPENUV_LISTENER] = {} + + if DOMAIN not in config: + return True conf = config[DOMAIN] - api_key = conf[CONF_API_KEY] - elevation = conf.get(CONF_ELEVATION, hass.config.elevation) latitude = conf.get(CONF_LATITUDE, hass.config.latitude) longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + elevation = conf.get(CONF_ELEVATION, hass.config.elevation) + + identifier = '{0}, {1}'.format(latitude, longitude) + + if identifier not in configured_instances(hass): + hass.async_add_job( + hass.config_entries.flow.async_init( + DOMAIN, + context={'source': SOURCE_IMPORT}, + data={ + CONF_API_KEY: conf[CONF_API_KEY], + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_ELEVATION: elevation, + CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS], + CONF_SENSORS: conf[CONF_SENSORS], + })) + + hass.data[DOMAIN][CONF_SCAN_INTERVAL] = conf[CONF_SCAN_INTERVAL] + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up OpenUV as config entry.""" + from pyopenuv import Client + from pyopenuv.errors import OpenUvError try: websession = aiohttp_client.async_get_clientsession(hass) openuv = OpenUV( Client( - api_key, latitude, longitude, websession, altitude=elevation), - conf[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] + - conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS]) + config_entry.data[CONF_API_KEY], + config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + websession, + altitude=config_entry.data.get( + CONF_ELEVATION, hass.config.elevation)), + config_entry.data.get(CONF_BINARY_SENSORS, {}).get( + CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS)), + config_entry.data.get(CONF_SENSORS, {}).get( + CONF_MONITORED_CONDITIONS, list(SENSORS))) await openuv.async_update() - hass.data[DOMAIN] = openuv + hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv except OpenUvError as err: _LOGGER.error('An error occurred: %s', str(err)) hass.components.persistent_notification.create( @@ -125,13 +166,9 @@ async def async_setup(hass, config): notification_id=NOTIFICATION_ID) return False - for component, schema in [ - ('binary_sensor', conf[CONF_BINARY_SENSORS]), - ('sensor', conf[CONF_SENSORS]), - ]: - hass.async_create_task( - discovery.async_load_platform( - hass, component, DOMAIN, schema, config)) + for component in ('binary_sensor', 'sensor'): + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, component)) async def refresh_sensors(event_time): """Refresh OpenUV data.""" @@ -139,7 +176,25 @@ async def async_setup(hass, config): await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) - async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL]) + hass.data[DOMAIN][DATA_OPENUV_LISTENER][ + config_entry.entry_id] = async_track_time_interval( + hass, refresh_sensors, + hass.data[DOMAIN].get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an OpenUV config entry.""" + for component in ('binary_sensor', 'sensor'): + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + + hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) + + remove_listener = hass.data[DOMAIN][DATA_OPENUV_LISTENER].pop( + config_entry.entry_id) + remove_listener() return True @@ -147,19 +202,20 @@ async def async_setup(hass, config): class OpenUV: """Define a generic OpenUV object.""" - def __init__(self, client, monitored_conditions): + def __init__(self, client, binary_sensor_conditions, sensor_conditions): """Initialize.""" - self._monitored_conditions = monitored_conditions + self.binary_sensor_conditions = binary_sensor_conditions self.client = client self.data = {} + self.sensor_conditions = sensor_conditions async def async_update(self): """Update sensor/binary sensor data.""" - if TYPE_PROTECTION_WINDOW in self._monitored_conditions: + if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: data = await self.client.uv_protection_window() self.data[DATA_PROTECTION_WINDOW] = data - if any(c in self._monitored_conditions for c in SENSORS): + if any(c in self.sensor_conditions for c in SENSORS): data = await self.client.uv_index() self.data[DATA_UV] = data diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py new file mode 100644 index 00000000000..6d7ae0f65bd --- /dev/null +++ b/homeassistant/components/openuv/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow to configure the OpenUV component.""" + +from collections import OrderedDict + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.const import ( + CONF_API_KEY, CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE) +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN + + +@callback +def configured_instances(hass): + """Return a set of configured OpenUV instances.""" + return set( + '{0}, {1}'.format( + entry.data[CONF_LATITUDE], entry.data[CONF_LONGITUDE]) + for entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class OpenUvFlowHandler(config_entries.ConfigFlow): + """Handle an OpenUV config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the config flow.""" + pass + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + from pyopenuv.util import validate_api_key + + errors = {} + + if user_input is not None: + identifier = '{0}, {1}'.format( + user_input.get(CONF_LATITUDE, self.hass.config.latitude), + user_input.get(CONF_LONGITUDE, self.hass.config.longitude)) + + if identifier in configured_instances(self.hass): + errors['base'] = 'identifier_exists' + else: + websession = aiohttp_client.async_get_clientsession(self.hass) + api_key_validation = await validate_api_key( + user_input[CONF_API_KEY], websession) + if api_key_validation: + return self.async_create_entry( + title=identifier, + data=user_input, + ) + errors['base'] = 'invalid_api_key' + + data_schema = OrderedDict() + data_schema[vol.Required(CONF_API_KEY)] = str + data_schema[vol.Optional(CONF_LATITUDE)] = cv.latitude + data_schema[vol.Optional(CONF_LONGITUDE)] = cv.longitude + data_schema[vol.Optional(CONF_ELEVATION)] = vol.Coerce(float) + + return self.async_show_form( + step_id='user', + data_schema=vol.Schema(data_schema), + errors=errors, + ) diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py new file mode 100644 index 00000000000..1aa3d2abcaa --- /dev/null +++ b/homeassistant/components/openuv/const.py @@ -0,0 +1,3 @@ +"""Define constants for the OpenUV component.""" + +DOMAIN = 'openuv' diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json new file mode 100644 index 00000000000..9c5af45619e --- /dev/null +++ b/homeassistant/components/openuv/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "title": "OpenUV", + "step": { + "user": { + "title": "Fill in your information", + "data": { + "api_key": "OpenUV API Key", + "elevation": "Elevation", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "identifier_exists": "Coordinates already registered", + "invalid_api_key": "Invalid API key" + } + } +} diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 2850a5f96cd..6b8fd68bc26 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -6,10 +6,12 @@ https://home-assistant.io/components/persistent_notification/ """ import asyncio import logging +from collections import OrderedDict from typing import Awaitable import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.core import callback, HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.loader import bind_hass @@ -20,13 +22,17 @@ from homeassistant.util import slugify ATTR_MESSAGE = 'message' ATTR_NOTIFICATION_ID = 'notification_id' ATTR_TITLE = 'title' +ATTR_STATUS = 'status' DOMAIN = 'persistent_notification' ENTITY_ID_FORMAT = DOMAIN + '.{}' +EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = 'persistent_notifications_updated' + SERVICE_CREATE = 'create' SERVICE_DISMISS = 'dismiss' +SERVICE_MARK_READ = 'mark_read' SCHEMA_SERVICE_CREATE = vol.Schema({ vol.Required(ATTR_MESSAGE): cv.template, @@ -38,11 +44,21 @@ SCHEMA_SERVICE_DISMISS = vol.Schema({ vol.Required(ATTR_NOTIFICATION_ID): cv.string, }) +SCHEMA_SERVICE_MARK_READ = vol.Schema({ + vol.Required(ATTR_NOTIFICATION_ID): cv.string, +}) DEFAULT_OBJECT_ID = 'notification' _LOGGER = logging.getLogger(__name__) STATE = 'notifying' +STATUS_UNREAD = 'unread' +STATUS_READ = 'read' + +WS_TYPE_GET_NOTIFICATIONS = 'persistent_notification/get' +SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_NOTIFICATIONS, +}) @bind_hass @@ -76,7 +92,7 @@ def async_create(hass: HomeAssistant, message: str, title: str = None, @callback @bind_hass -def async_dismiss(hass, notification_id): +def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: """Remove a notification.""" data = {ATTR_NOTIFICATION_ID: notification_id} @@ -86,6 +102,9 @@ def async_dismiss(hass, notification_id): @asyncio.coroutine def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: """Set up the persistent notification component.""" + persistent_notifications = OrderedDict() + hass.data[DOMAIN] = {'notifications': persistent_notifications} + @callback def create_service(call): """Handle a create notification service call.""" @@ -98,6 +117,8 @@ def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: else: entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, hass=hass) + notification_id = entity_id.split('.')[1] + attr = {} if title is not None: try: @@ -120,18 +141,72 @@ def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: hass.states.async_set(entity_id, STATE, attr) + # Store notification and fire event + # This will eventually replace state machine storage + persistent_notifications[entity_id] = { + ATTR_MESSAGE: message, + ATTR_NOTIFICATION_ID: notification_id, + ATTR_STATUS: STATUS_UNREAD, + ATTR_TITLE: title, + } + + hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + @callback def dismiss_service(call): """Handle the dismiss notification service call.""" notification_id = call.data.get(ATTR_NOTIFICATION_ID) entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) + if entity_id not in persistent_notifications: + return + hass.states.async_remove(entity_id) + del persistent_notifications[entity_id] + hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + + @callback + def mark_read_service(call): + """Handle the mark_read notification service call.""" + notification_id = call.data.get(ATTR_NOTIFICATION_ID) + entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) + + if entity_id not in persistent_notifications: + _LOGGER.error('Marking persistent_notification read failed: ' + 'Notification ID %s not found.', notification_id) + return + + persistent_notifications[entity_id][ATTR_STATUS] = STATUS_READ + hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + hass.services.async_register(DOMAIN, SERVICE_CREATE, create_service, SCHEMA_SERVICE_CREATE) hass.services.async_register(DOMAIN, SERVICE_DISMISS, dismiss_service, SCHEMA_SERVICE_DISMISS) + hass.services.async_register(DOMAIN, SERVICE_MARK_READ, mark_read_service, + SCHEMA_SERVICE_MARK_READ) + + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_NOTIFICATIONS, websocket_get_notifications, + SCHEMA_WS_GET + ) + return True + + +@callback +def websocket_get_notifications( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): + """Return a list of persistent_notifications.""" + connection.to_write.put_nowait( + websocket_api.result_message(msg['id'], [ + { + key: data[key] for key in (ATTR_NOTIFICATION_ID, ATTR_MESSAGE, + ATTR_STATUS, ATTR_TITLE) + } + for data in hass.data[DOMAIN]['notifications'].values() + ]) + ) diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index bbc6e07f2b0..5c56caf6470 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script.py @@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename import homeassistant.util.dt as dt_util -REQUIREMENTS = ['restrictedpython==4.0b4'] +REQUIREMENTS = ['restrictedpython==4.0b5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio.py index 0e67e15d5c0..27827da0182 100644 --- a/homeassistant/components/rachio.py +++ b/homeassistant/components/rachio.py @@ -6,14 +6,14 @@ https://home-assistant.io/components/rachio/ """ import asyncio import logging +from typing import Optional from aiohttp import web import voluptuous as vol -from typing import Optional from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send REQUIREMENTS = ['rachiopy==0.1.3'] @@ -22,11 +22,19 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'rachio' +SUPPORTED_DOMAINS = ['switch', 'binary_sensor'] + +# Manual run length +CONF_MANUAL_RUN_MINS = 'manual_run_mins' +DEFAULT_MANUAL_RUN_MINS = 10 CONF_CUSTOM_URL = 'hass_url_override' + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_CUSTOM_URL): cv.string, + vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS): + cv.positive_int, }) }, extra=vol.ALLOW_EXTRA) @@ -112,7 +120,7 @@ def setup(hass, config) -> bool: # Get the API user try: - person = RachioPerson(hass, rachio) + person = RachioPerson(hass, rachio, config[DOMAIN]) except AssertionError as error: _LOGGER.error("Could not reach the Rachio API: %s", error) return False @@ -126,17 +134,23 @@ def setup(hass, config) -> bool: # Enable component hass.data[DOMAIN] = person + + # Load platforms + for component in SUPPORTED_DOMAINS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + return True class RachioPerson: """Represent a Rachio user.""" - def __init__(self, hass, rachio): + def __init__(self, hass, rachio, config): """Create an object from the provided API instance.""" # Use API token to get user ID self._hass = hass self.rachio = rachio + self.config = config response = rachio.person.getInfo() assert int(response[0][KEY_STATUS]) == 200, "API key error" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 47d6e181c8f..a3cd2eebd8c 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -15,7 +15,6 @@ import logging import queue import threading import time - from typing import Any, Dict, Optional # noqa: F401 import voluptuous as vol diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 11ecb20f7aa..3fd2a5d4c44 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -70,63 +70,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, activity=None, entity_id=None): - """Turn all or specified remote on.""" - data = { - key: value for key, value in [ - (ATTR_ACTIVITY, activity), - (ATTR_ENTITY_ID, entity_id), - ] if value is not None} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, activity=None, entity_id=None): - """Turn all or specified remote off.""" - data = {} - if activity: - data[ATTR_ACTIVITY] = activity - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, activity=None, entity_id=None): - """Toggle all or specified remote.""" - data = {} - if activity: - data[ATTR_ACTIVITY] = activity - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def send_command(hass, command, entity_id=None, device=None, - num_repeats=None, delay_secs=None): - """Send a command to a device.""" - data = {ATTR_COMMAND: command} - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - if device: - data[ATTR_DEVICE] = device - - if num_repeats: - data[ATTR_NUM_REPEATS] = num_repeats - - if delay_secs: - data[ATTR_DELAY_SECS] = delay_secs - - hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) - - @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for remotes.""" diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 60dbb209039..b5bc97b7ffa 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -REQUIREMENTS = ['pyRFXtrx==0.22.1'] +REQUIREMENTS = ['pyRFXtrx==0.23'] DOMAIN = 'rfxtrx' diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 8771a84c1d6..2bcb1c8e16d 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, SERVICE_TURN_ON) -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -61,17 +60,6 @@ SCENE_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def activate(hass, entity_id=None): - """Activate a scene.""" - data = {} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - async def async_setup(hass, config): """Set up the scenes.""" logger = logging.getLogger(__name__) diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index 5af8f657206..b8fca6d8630 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -5,8 +5,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.components.scene import Scene +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] @@ -19,12 +21,17 @@ async def async_setup_platform(hass, config, async_add_entities, async def async_setup_entry(hass, config_entry, async_add_entities): """Set up scenes for deCONZ component.""" - scenes = hass.data[DATA_DECONZ].scenes - entities = [] + @callback + def async_add_scene(scenes): + """Add scene from deCONZ.""" + entities = [] + for scene in scenes: + entities.append(DeconzScene(scene)) + async_add_entities(entities) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_scene', async_add_scene)) - for scene in scenes.values(): - entities.append(DeconzScene(scene)) - async_add_entities(entities) + async_add_scene(hass.data[DATA_DECONZ].scenes.values()) class DeconzScene(Scene): diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index a45f8ba8930..16c9f65420c 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -15,7 +15,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_RELOAD, STATE_ON, CONF_ALIAS) -from homeassistant.core import split_entity_id from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -62,80 +61,41 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id, variables=None): - """Turn script on.""" - _, object_id = split_entity_id(entity_id) - - hass.services.call(DOMAIN, object_id, variables) - - -@bind_hass -def turn_off(hass, entity_id): - """Turn script on.""" - hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def toggle(hass, entity_id): - """Toggle the script.""" - hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def reload(hass): - """Reload script component.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) - - -@bind_hass -def async_reload(hass): - """Reload the scripts from config. - - Returns a coroutine object. - """ - return hass.services.async_call(DOMAIN, SERVICE_RELOAD) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Load the scripts from the configuration.""" component = EntityComponent( _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_SCRIPTS) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def reload_service(service): + async def reload_service(service): """Call a service to reload scripts.""" - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) - @asyncio.coroutine - def turn_on_service(service): + async def turn_on_service(service): """Call a service to turn script on.""" # We could turn on script directly here, but we only want to offer # one way to do it. Otherwise no easy way to detect invocations. var = service.data.get(ATTR_VARIABLES) for script in component.async_extract_from_service(service): - yield from hass.services.async_call(DOMAIN, script.object_id, var) + await hass.services.async_call(DOMAIN, script.object_id, var, + context=service.context) - @asyncio.coroutine - def turn_off_service(service): + async def turn_off_service(service): """Cancel a script.""" # Stopping a script is ok to be done in parallel - yield from asyncio.wait( + await asyncio.wait( [script.async_turn_off() for script in component.async_extract_from_service(service)], loop=hass.loop) - @asyncio.coroutine - def toggle_service(service): + async def toggle_service(service): """Toggle a script.""" for script in component.async_extract_from_service(service): - yield from script.async_toggle() + await script.async_toggle(context=service.context) hass.services.async_register(DOMAIN, SERVICE_RELOAD, reload_service, schema=RELOAD_SERVICE_SCHEMA) @@ -149,18 +109,17 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _async_process_config(hass, config, component): - """Process group configuration.""" - @asyncio.coroutine - def service_handler(service): +async def _async_process_config(hass, config, component): + """Process script configuration.""" + async def service_handler(service): """Execute a service call to script.