diff --git a/.coveragerc b/.coveragerc index bb0be2d9433..ca36f4a8dbb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -116,11 +116,14 @@ omit = homeassistant/components/google.py homeassistant/components/*/google.py + homeassistant/components/habitica/* + homeassistant/components/*/habitica.py + homeassistant/components/hangouts/__init__.py homeassistant/components/hangouts/const.py homeassistant/components/hangouts/hangouts_bot.py homeassistant/components/hangouts/hangups_utils.py - homeassistant/components/*/hangouts.py + homeassistant/components/*/hangouts.py homeassistant/components/hdmi_cec.py homeassistant/components/*/hdmi_cec.py @@ -142,12 +145,12 @@ omit = 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 @@ -225,7 +228,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 @@ -374,6 +377,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 @@ -411,6 +415,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 @@ -759,6 +764,7 @@ omit = homeassistant/components/sensor/uscis.py homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/viaggiatreno.py + homeassistant/components/sensor/volkszaehler.py homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waze_travel_time.py homeassistant/components/sensor/whois.py @@ -789,6 +795,8 @@ 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 homeassistant/components/switch/transmission.py 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/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..fb4700c806f 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 @@ -128,11 +129,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 +195,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 +242,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 +318,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/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/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/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/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 e273d7d6f6a..0d6d811db70 100644 --- a/homeassistant/components/asterisk_mbox.py +++ b/homeassistant/components/asterisk_mbox.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) -REQUIREMENTS = ['asterisk_mbox==0.4.0'] +REQUIREMENTS = ['asterisk_mbox==0.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json index e8a8037c39a..b8d10dc89d0 100644 --- a/homeassistant/components/auth/.translations/fr.json +++ b/homeassistant/components/auth/.translations/fr.json @@ -2,7 +2,7 @@ "mfa_setup": { "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/sv.json b/homeassistant/components/auth/.translations/sv.json new file mode 100644 index 00000000000..cf8227c09a3 --- /dev/null +++ b/homeassistant/components/auth/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "mfa_setup": { + "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/__init__.py b/homeassistant/components/auth/__init__.py index a87e646761c..bee72d8e4fc 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,67 @@ 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", + "client_icon": null, + "lifespan": 365 +} + +Result will be a long-lived access token: + +{ + "id": 11, + "type": "result", + "success": true, + "result": "ABCDEFGH" +} + """ import logging import uuid @@ -63,8 +126,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 +148,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 +187,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 +237,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 +267,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 +303,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 +314,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 +342,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 +449,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/login_flow.py b/homeassistant/components/auth/login_flow.py index a518bdde415..73a739c2960 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, diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c6c0af90d15..43fd4cedb88 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -158,27 +158,26 @@ def async_reload(hass): 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 +185,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 +197,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 +269,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 +294,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 +345,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 +360,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 +402,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 +438,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 +446,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/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 1fb62124407..d2ca9e7c5e8 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -54,6 +54,11 @@ class DeconzBinarySensor(BinarySensorDevice): self._sensor.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id + async def async_will_remove_from_hass(self) -> None: + """Disconnect sensor object when removed.""" + self._sensor.remove_callback(self.async_update_callback) + self._sensor = None + @callback def async_update_callback(self, reason): """Update the sensor's state. 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/camera/__init__.py b/homeassistant/components/camera/__init__.py index 76860702165..a8a486013d4 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -142,6 +142,68 @@ def async_snapshot(hass, filename, entity_id=None): @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 +217,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 +345,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/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/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/fr.json b/homeassistant/components/cast/.translations/fr.json index d3b95121de6..99feeb3c898 100644 --- a/homeassistant/components/cast/.translations/fr.json +++ b/homeassistant/components/cast/.translations/fr.json @@ -2,7 +2,7 @@ "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": { 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/nest.py b/homeassistant/components/climate/nest.py index 321559f10ee..bc63512fcf3 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -8,7 +8,8 @@ import logging import voluptuous as vol -from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE +from homeassistant.components.nest import ( + DATA_NEST, SIGNAL_NEST_UPDATE, DOMAIN as NEST_DOMAIN) from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -127,6 +128,19 @@ class NestThermostat(ClimateDevice): """Return unique ID for this device.""" return self.device.serial + @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': "Thermostat", + 'sw_version': self.device.software_version, + } + @property def name(self): """Return the name of the nest, if any.""" diff --git a/homeassistant/components/climate/opentherm_gw.py b/homeassistant/components/climate/opentherm_gw.py new file mode 100644 index 00000000000..c1f7afa61b0 --- /dev/null +++ b/homeassistant/components/climate/opentherm_gw.py @@ -0,0 +1,189 @@ +""" +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) + cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE) + if ch_active: + 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/config/config_entries.py b/homeassistant/components/config/config_entries.py index 04d2c713cdc..e0c0e7daaf4 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.""" diff --git a/homeassistant/components/cover/insteon.py b/homeassistant/components/cover/insteon.py new file mode 100644 index 00000000000..f0cf93c13e9 --- /dev/null +++ b/homeassistant/components/cover/insteon.py @@ -0,0 +1,73 @@ +""" +Support for Insteon covers via PowerLinc Modem. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/cover.insteon/ +""" +import logging +import math + +from homeassistant.components.insteon import InsteonEntity +from homeassistant.components.cover import (CoverDevice, ATTR_POSITION, + SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_SET_POSITION) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['insteon'] +SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Insteon platform.""" + if not discovery_info: + return + + insteon_modem = hass.data['insteon'].get('modem') + + address = discovery_info['address'] + device = insteon_modem.devices[address] + state_key = discovery_info['state_key'] + + _LOGGER.debug('Adding device %s entity %s to Cover platform', + device.address.hex, device.states[state_key].name) + + new_entity = InsteonCoverDevice(device, state_key) + + async_add_entities([new_entity]) + + +class InsteonCoverDevice(InsteonEntity, CoverDevice): + """A Class for an Insteon device.""" + + @property + def current_cover_position(self): + """Return the current cover position.""" + return int(math.ceil(self._insteon_device_state.value*100/255)) + + @property + def supported_features(self): + """Return the supported features for this entity.""" + return SUPPORTED_FEATURES + + @property + def is_closed(self): + """Return the boolean response if the node is on.""" + return bool(self.current_cover_position) + + async def async_open_cover(self, **kwargs): + """Open device.""" + self._insteon_device_state.open() + + async def async_close_cover(self, **kwargs): + """Close device.""" + self._insteon_device_state.close() + + async def async_set_cover_position(self, **kwargs): + """Set the cover position.""" + position = int(kwargs[ATTR_POSITION]*255/100) + if position == 0: + self._insteon_device_state.close() + else: + self._insteon_device_state.set_position(position) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index bedc041fccc..413794505db 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -8,17 +8,25 @@ import logging import voluptuous as vol -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED) + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, + STATE_OPENING) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pymyq==0.0.11'] +REQUIREMENTS = ['pymyq==0.0.15'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'myq' +MYQ_TO_HASS = { + 'closed': STATE_CLOSED, + 'closing': STATE_CLOSING, + 'opening': STATE_OPENING +} + NOTIFICATION_ID = 'myq_notification' NOTIFICATION_TITLE = 'MyQ Cover Setup' @@ -87,7 +95,17 @@ class MyQDevice(CoverDevice): @property def is_closed(self): """Return true if cover is closed, else False.""" - return self._status == STATE_CLOSED + return MYQ_TO_HASS[self._status] == STATE_CLOSED + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return MYQ_TO_HASS[self._status] == STATE_CLOSING + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return MYQ_TO_HASS[self._status] == STATE_OPENING def close_cover(self, **kwargs): """Issue close command to cover.""" @@ -97,6 +115,16 @@ class MyQDevice(CoverDevice): """Issue open command to cover.""" self.myq.open_device(self.device_id) + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self.device_id + def update(self): """Update status of cover.""" self._status = self.myq.get_status(self.device_id) 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/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/__init__.py b/homeassistant/components/deconz/__init__.py index a4edc009ea1..6ed0a6e2c11 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -24,7 +24,7 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==44'] +REQUIREMENTS = ['pydeconz==47'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -46,6 +46,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 +86,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 @@ -149,16 +153,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): """ @@ -179,15 +227,22 @@ async def async_unload_entry(hass, config_entry): deconz = hass.data.pop(DOMAIN) hass.services.async_remove(DOMAIN, SERVICE_DECONZ) deconz.close() - for component in ['binary_sensor', 'light', 'scene', 'sensor']: + + for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']: await hass.config_entries.async_forward_entry_unload( config_entry, component) + dispatchers = hass.data[DATA_DECONZ_UNSUB] for unsub_dispatcher in dispatchers: unsub_dispatcher() hass.data[DATA_DECONZ_UNSUB] = [] - hass.data[DATA_DECONZ_EVENT] = [] + + for event in hass.data[DATA_DECONZ_EVENT]: + event.async_will_remove_from_hass() + hass.data[DATA_DECONZ_EVENT].remove(event) + hass.data[DATA_DECONZ_ID] = [] + return True @@ -206,6 +261,12 @@ class DeconzEvent: self._event = 'deconz_{}'.format(CONF_EVENT) self._id = slugify(self._device.name) + @callback + def async_will_remove_from_hass(self) -> None: + """Disconnect event object when removed.""" + self._device.remove_callback(self.async_update_callback) + self._device = None + @callback def async_update_callback(self, reason): """Fire the event if reason is that state is updated.""" 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_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 2ca519d225c..d22a1ba7c1f 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -12,7 +12,8 @@ import homeassistant.helpers.config_validation as cv 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, - load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH) + load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW, SOURCE_TYPE_BLUETOOTH, + DOMAIN) import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -79,7 +80,13 @@ def setup_scanner(hass, config, see, discovery_info=None): request_rssi = config.get(CONF_REQUEST_RSSI, False) - def update_bluetooth(now): + def update_bluetooth(_): + """Update Bluetooth and set timer for the next update.""" + update_bluetooth_once() + track_point_in_utc_time( + hass, update_bluetooth, dt_util.utcnow() + interval) + + def update_bluetooth_once(): """Lookup Bluetooth device and update status.""" try: if track_new: @@ -99,9 +106,14 @@ def setup_scanner(hass, config, see, discovery_info=None): see_device(mac, result, rssi) except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") - track_point_in_utc_time( - hass, update_bluetooth, dt_util.utcnow() + interval) + + def handle_update_bluetooth(call): + """Update bluetooth devices on demand.""" + update_bluetooth_once() update_bluetooth(dt_util.utcnow()) + hass.services.register( + DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) + return True diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 8c21e71bd30..170d3de6800 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -15,7 +15,7 @@ from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME 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'] @@ -92,7 +92,7 @@ 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, } self.see( diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 72f61ddf1eb..6dd4be7ecec 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ 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==20180903.0'] +REQUIREMENTS = ['home-assistant-frontend==20180916.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py new file mode 100644 index 00000000000..67ed9520fa4 --- /dev/null +++ b/homeassistant/components/geo_location/__init__.py @@ -0,0 +1,68 @@ +""" +Geo Location component. + +This component covers platforms that deal with external events that contain +a geo location related to the installed HA instance. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/geo_location/ +""" +import logging +from datetime import timedelta +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 + +_LOGGER = logging.getLogger(__name__) + +ATTR_DISTANCE = 'distance' +DOMAIN = 'geo_location' +ENTITY_ID_FORMAT = DOMAIN + '.{}' +GROUP_NAME_ALL_EVENTS = 'All Geo Location Events' +SCAN_INTERVAL = timedelta(seconds=60) + + +async def async_setup(hass, config): + """Set up this component.""" + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS) + await component.async_setup(config) + return True + + +class GeoLocationEvent(Entity): + """This represents an external event with an associated geo location.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.distance is not None: + return round(self.distance, 1) + return None + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return None + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return None + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return None + + @property + def state_attributes(self): + """Return the state attributes of this external event.""" + data = {} + if self.latitude is not None: + data[ATTR_LATITUDE] = round(self.latitude, 5) + if self.longitude is not None: + data[ATTR_LONGITUDE] = round(self.longitude, 5) + return data diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py new file mode 100644 index 00000000000..8e8d8211086 --- /dev/null +++ b/homeassistant/components/geo_location/demo.py @@ -0,0 +1,132 @@ +""" +Demo platform for the geo location component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +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 +from homeassistant.helpers.event import track_time_interval + +_LOGGER = logging.getLogger(__name__) + +AVG_KM_PER_DEGREE = 111.0 +DEFAULT_UNIT_OF_MEASUREMENT = "km" +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) +MAX_RADIUS_IN_KM = 50 +NUMBER_OF_DEMO_DEVICES = 5 + +EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off", + "Structure Fire", "Fire Alarm", "Thunderstorm", "Tornado", + "Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm", + "Earthquake", "Tsunami"] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Demo geo locations.""" + DemoManager(hass, add_entities) + + +class DemoManager: + """Device manager for demo geo location events.""" + + def __init__(self, hass, add_entities): + """Initialise the demo geo location event manager.""" + self._hass = hass + self._add_entities = add_entities + self._managed_devices = [] + self._update(count=NUMBER_OF_DEMO_DEVICES) + self._init_regular_updates() + + def _generate_random_event(self): + """Generate a random event in vicinity of this HA instance.""" + home_latitude = self._hass.config.latitude + home_longitude = self._hass.config.longitude + + # Approx. 111km per degree (north-south). + radius_in_degrees = random.random() * MAX_RADIUS_IN_KM / \ + AVG_KM_PER_DEGREE + radius_in_km = radius_in_degrees * AVG_KM_PER_DEGREE + angle = random.random() * 2 * pi + # Compute coordinates based on radius and angle. Adjust longitude value + # based on HA's latitude. + latitude = home_latitude + radius_in_degrees * sin(angle) + longitude = home_longitude + radius_in_degrees * cos(angle) / \ + cos(radians(home_latitude)) + + event_name = random.choice(EVENT_NAMES) + return DemoGeoLocationEvent(event_name, radius_in_km, latitude, + longitude, DEFAULT_UNIT_OF_MEASUREMENT) + + def _init_regular_updates(self): + """Schedule regular updates based on configured time interval.""" + track_time_interval(self._hass, lambda now: self._update(), + DEFAULT_UPDATE_INTERVAL) + + def _update(self, count=1): + """Remove events and add new random events.""" + # Remove devices. + for _ in range(1, count + 1): + if self._managed_devices: + device = random.choice(self._managed_devices) + if device: + _LOGGER.debug("Removing %s", device) + self._managed_devices.remove(device) + self._hass.add_job(device.async_remove()) + # Generate new devices from events. + new_devices = [] + for _ in range(1, count + 1): + new_device = self._generate_random_event() + _LOGGER.debug("Adding %s", new_device) + new_devices.append(new_device) + self._managed_devices.append(new_device) + self._add_entities(new_devices) + + +class DemoGeoLocationEvent(GeoLocationEvent): + """This represents a demo geo location event.""" + + def __init__(self, name, distance, latitude, longitude, + unit_of_measurement): + """Initialize entity with data provided.""" + self._name = name + self._distance = distance + self._latitude = latitude + self._longitude = longitude + self._unit_of_measurement = unit_of_measurement + + @property + def name(self) -> Optional[str]: + """Return the name of the event.""" + return self._name + + @property + def should_poll(self): + """No polling needed for a demo geo location event.""" + return False + + @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 self._unit_of_measurement diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py new file mode 100644 index 00000000000..44b9e392157 --- /dev/null +++ b/homeassistant/components/habitica/__init__.py @@ -0,0 +1,158 @@ +""" +The Habitica API component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/habitica/ +""" + +import logging +from collections import namedtuple + +import voluptuous as vol +from homeassistant.const import \ + CONF_NAME, CONF_URL, CONF_SENSORS, CONF_PATH, CONF_API_KEY +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import \ + config_validation as cv, discovery + +REQUIREMENTS = ['habitipy==0.2.0'] +_LOGGER = logging.getLogger(__name__) +DOMAIN = "habitica" + +CONF_API_USER = "api_user" + +ST = SensorType = namedtuple('SensorType', [ + "name", "icon", "unit", "path" +]) + +SENSORS_TYPES = { + 'name': ST('Name', None, '', ["profile", "name"]), + 'hp': ST('HP', 'mdi:heart', 'HP', ["stats", "hp"]), + 'maxHealth': ST('max HP', 'mdi:heart', 'HP', ["stats", "maxHealth"]), + 'mp': ST('Mana', 'mdi:auto-fix', 'MP', ["stats", "mp"]), + 'maxMP': ST('max Mana', 'mdi:auto-fix', 'MP', ["stats", "maxMP"]), + 'exp': ST('EXP', 'mdi:star', 'EXP', ["stats", "exp"]), + 'toNextLevel': ST( + 'Next Lvl', 'mdi:star', 'EXP', ["stats", "toNextLevel"]), + 'lvl': ST( + 'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ["stats", "lvl"]), + 'gp': ST('Gold', 'mdi:coin', 'Gold', ["stats", "gp"]), + 'class': ST('Class', 'mdi:sword', '', ["stats", "class"]) +} + +INSTANCE_SCHEMA = vol.Schema({ + vol.Optional(CONF_URL, default='https://habitica.com'): cv.url, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_API_USER): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): + vol.All( + cv.ensure_list, + vol.Unique(), + [vol.In(list(SENSORS_TYPES))]) +}) + +has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name +# because we want a handy alias + + +def has_all_unique_users(value): + """Validate that all `api_user`s are unique.""" + api_users = [user[CONF_API_USER] for user in value] + has_unique_values(api_users) + return value + + +def has_all_unique_users_names(value): + """Validate that all user's names are unique and set if any is set.""" + names = [user.get(CONF_NAME) for user in value] + if None in names and any(name is not None for name in names): + raise vol.Invalid( + 'user names of all users must be set if any is set') + if not all(name is None for name in names): + has_unique_values(names) + return value + + +INSTANCE_LIST_SCHEMA = vol.All( + cv.ensure_list, + has_all_unique_users, + has_all_unique_users_names, + [INSTANCE_SCHEMA]) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: INSTANCE_LIST_SCHEMA +}, extra=vol.ALLOW_EXTRA) + +SERVICE_API_CALL = 'api_call' +ATTR_NAME = CONF_NAME +ATTR_PATH = CONF_PATH +ATTR_ARGS = "args" +EVENT_API_CALL_SUCCESS = "{0}_{1}_{2}".format( + DOMAIN, SERVICE_API_CALL, "success") + +SERVICE_API_CALL_SCHEMA = vol.Schema({ + vol.Required(ATTR_NAME): str, + vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]), + vol.Optional(ATTR_ARGS): dict +}) + + +async def async_setup(hass, config): + """Set up the habitica service.""" + conf = config[DOMAIN] + data = hass.data[DOMAIN] = {} + websession = async_get_clientsession(hass) + from habitipy.aio import HabitipyAsync + + class HAHabitipyAsync(HabitipyAsync): + """Closure API class to hold session.""" + + def __call__(self, **kwargs): + return super().__call__(websession, **kwargs) + + for instance in conf: + url = instance[CONF_URL] + username = instance[CONF_API_USER] + password = instance[CONF_API_KEY] + name = instance.get(CONF_NAME) + config_dict = {"url": url, "login": username, "password": password} + api = HAHabitipyAsync(config_dict) + user = await api.user.get() + if name is None: + name = user['profile']['name'] + data[name] = api + if CONF_SENSORS in instance: + hass.async_create_task( + discovery.async_load_platform( + hass, "sensor", DOMAIN, + {"name": name, "sensors": instance[CONF_SENSORS]}, + config)) + + async def handle_api_call(call): + name = call.data[ATTR_NAME] + path = call.data[ATTR_PATH] + api = hass.data[DOMAIN].get(name) + if api is None: + _LOGGER.error( + "API_CALL: User '%s' not configured", name) + return + try: + for element in path: + api = api[element] + except KeyError: + _LOGGER.error( + "API_CALL: Path %s is invalid" + " for api on '{%s}' element", path, element) + return + kwargs = call.data.get(ATTR_ARGS, {}) + data = await api(**kwargs) + hass.bus.async_fire(EVENT_API_CALL_SUCCESS, { + "name": name, "path": path, "data": data + }) + + hass.services.async_register( + DOMAIN, SERVICE_API_CALL, + handle_api_call, + schema=SERVICE_API_CALL_SCHEMA) + return True diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml new file mode 100644 index 00000000000..a063b1577f5 --- /dev/null +++ b/homeassistant/components/habitica/services.yaml @@ -0,0 +1,15 @@ +# Describes the format for Habitica service + +--- +api_call: + description: Call Habitica api + fields: + name: + description: Habitica's username to call for + example: 'xxxNotAValidNickxxx' + path: + description: "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks" + example: '["tasks", "user", "post"]' + args: + description: Any additional json or url parameter arguments. See apidoc mentioned for path. Example uses same api endpoint + example: '{"text": "Use API from Home Assistant", "type": "todo"}' diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json index 53759f9b534..00a7d5fd80d 100644 --- a/homeassistant/components/hangouts/.translations/fr.json +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -5,10 +5,15 @@ "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": { 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/strings.json b/homeassistant/components/hangouts/strings.json index 7e54586b810..dd421fee57a 100644 --- a/homeassistant/components/hangouts/strings.json +++ b/homeassistant/components/hangouts/strings.json @@ -6,7 +6,7 @@ }, "error": { "invalid_login": "Invalid Login, please try again.", - "invalid_2fa": "Invalid 2 Factor Authorization, please try again.", + "invalid_2fa": "Invalid 2 Factor Authentication, please try again.", "invalid_2fa_method": "Invalid 2FA Method (Verify on Phone)." }, "step": { @@ -23,7 +23,7 @@ "2fa": "2FA Pin" }, "description": "", - "title": "2-Factor-Authorization" + "title": "2-Factor-Authentication" } }, "title": "Google Hangouts" diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 53c8e267016..2b517652ad7 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -77,7 +77,7 @@ HM_DEVICE_TYPES = { 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor', - 'IPKeySwitchPowermeter'], + 'IPKeySwitchPowermeter', 'IPThermostatWall230V'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', 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/fr.json b/homeassistant/components/homematicip_cloud/.translations/fr.json index c10cb519133..6cab0993c01 100644 --- a/homeassistant/components/homematicip_cloud/.translations/fr.json +++ b/homeassistant/components/homematicip_cloud/.translations/fr.json @@ -1,12 +1,16 @@ { "config": { "abort": { - "unknown": "Une erreur inconnue s'est produite" + "already_configured": "Le point d'acc\u00e8s est d\u00e9j\u00e0 configur\u00e9", + "conection_aborted": "Impossible de se connecter au serveur HMIP", + "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 +18,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/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/sv.json b/homeassistant/components/homematicip_cloud/.translations/sv.json index 945dca8a277..4e8aac999de 100644 --- a/homeassistant/components/homematicip_cloud/.translations/sv.json +++ b/homeassistant/components/homematicip_cloud/.translations/sv.json @@ -3,6 +3,7 @@ "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/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/__init__.py b/homeassistant/components/hue/__init__.py index c04380e1303..38b521078f4 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -11,7 +11,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_FILENAME, CONF_HOST -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import ( + aiohttp_client, config_validation as cv, device_registry as dr) from .const import DOMAIN, API_NUPNP from .bridge import HueBridge @@ -132,7 +133,28 @@ async def async_setup_entry(hass, entry): bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) hass.data[DOMAIN][host] = bridge - return await bridge.async_setup() + + if not await bridge.async_setup(): + return False + + config = bridge.api.config + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry=entry.entry_id, + connections={ + (dr.CONNECTION_NETWORK_MAC, config.mac) + }, + identifiers={ + (DOMAIN, config.bridgeid) + }, + manufacturer='Signify', + name=config.name, + # Not yet exposed as properties in aiohue + model=config.raw['modelid'], + sw_version=config.raw['swversion'], + ) + + return True async def async_unload_entry(hass, entry): diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 212cdbac3b8..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.12.3'] +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: @@ -358,6 +372,8 @@ class IPDB: def __init__(self): """Create the INSTEON Product Database (IPDB).""" + from insteonplm.states.cover import Cover + from insteonplm.states.onOff import (OnOffSwitch, OnOffSwitch_OutletTop, OnOffSwitch_OutletBottom, @@ -383,7 +399,9 @@ class IPDB: X10AllLightsOnSensor, X10AllLightsOffSensor) - self.states = [State(OnOffSwitch_OutletTop, 'switch'), + self.states = [State(Cover, 'cover'), + + State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), State(OnOffSwitch, 'switch'), @@ -470,11 +488,10 @@ class InsteonEntity(Entity): return attributes @callback - def async_entity_update(self, deviceid, statename, val): + def async_entity_update(self, deviceid, group, val): """Receive notification from transport that new data exists.""" - _LOGGER.debug('Received update for device %s group %d statename %s', - self.address, self.group, - self._insteon_device_state.name) + _LOGGER.debug('Received update for device %s group %d value %s', + deviceid.human, group, val) self.async_schedule_update_ha_state() @asyncio.coroutine 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/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/ko.json b/homeassistant/components/ios/.translations/ko.json new file mode 100644 index 00000000000..6d69ea3126c --- /dev/null +++ b/homeassistant/components/ios/.translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 Home Assistant iOS \uad6c\uc131\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc124\uc815\uc744 \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/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/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/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/light/deconz.py b/homeassistant/components/light/deconz.py index 412cf8693e5..ff3fe609924 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -82,6 +82,11 @@ class DeconzLight(Light): self._light.register_async_callback(self.async_update_callback) self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._light.deconz_id + async def async_will_remove_from_hass(self) -> None: + """Disconnect light object when removed.""" + self._light.remove_callback(self.async_update_callback) + self._light = None + @callback def async_update_callback(self, reason): """Update the light's state.""" diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 2a51423a7a8..6f6e0ed617e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -285,6 +285,25 @@ class HueLight(Light): """Return the list of supported effects.""" return [EFFECT_COLORLOOP, EFFECT_RANDOM] + @property + def device_info(self): + """Return the device info.""" + if self.light.type in ('LightGroup', 'Room'): + return None + + return { + 'identifiers': { + (hue.DOMAIN, self.unique_id) + }, + 'name': self.name, + 'manufacturer': self.light.manufacturername, + # productname added in Hue Bridge API 1.24 + # (published 03/05/2018) + 'model': self.light.productname or self.light.modelid, + # Not yet exposed as properties in aiohue + 'sw_version': self.light.raw['swversion'], + } + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {'on': True} diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 225f0f510ad..64331411f7f 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -54,6 +54,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 +80,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, @@ -111,6 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, 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 ( @@ -159,14 +162,15 @@ async def async_setup_platform(hass, config, async_add_entities, class MqttLight(MqttAvailability, 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): """Initialize MQTT light.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) self._name = name + self._unique_id = unique_id self._effect_list = effect_list self._topic = topic self._qos = qos @@ -392,6 +396,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/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/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 6a648e4dc47..0c5dabb6eeb 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -4,31 +4,33 @@ 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__) + +CONTENT_TYPE_MPEG = 'audio/mpeg' + DEPENDENCIES = ['http'] DOMAIN = 'mailbox' + EVENT = 'mailbox_updated' -CONTENT_TYPE_MPEG = 'audio/mpeg' + SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) @asyncio.coroutine diff --git a/homeassistant/components/mailbox/asterisk_mbox.py b/homeassistant/components/mailbox/asterisk_mbox.py index 47d59234d7d..29b34f3e512 100644 --- a/homeassistant/components/mailbox/asterisk_mbox.py +++ b/homeassistant/components/mailbox/asterisk_mbox.py @@ -7,17 +7,18 @@ https://home-assistant.io/components/mailbox.asteriskvm/ import asyncio import logging -from homeassistant.core import callback from homeassistant.components.asterisk_mbox import DOMAIN -from homeassistant.components.mailbox import (Mailbox, CONTENT_TYPE_MPEG, - StreamError) +from homeassistant.components.mailbox import ( + CONTENT_TYPE_MPEG, Mailbox, StreamError) +from homeassistant.core import callback 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 diff --git a/homeassistant/components/mailbox/demo.py b/homeassistant/components/mailbox/demo.py index 8096a4fabb7..e0d2618ac4e 100644 --- a/homeassistant/components/mailbox/demo.py +++ b/homeassistant/components/mailbox/demo.py @@ -5,16 +5,16 @@ 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" @@ -38,11 +38,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 diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 8f2abb9be19..0d7d76c4447 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.10'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 7c49b095c66..831009ed8bf 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 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 ae9589c7886..088aef82373 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -7,26 +7,27 @@ https://home-assistant.io/components/media_player.cast/ import asyncio 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,10 +58,14 @@ 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]), }) +CONNECTION_RETRY = 3 +CONNECTION_RETRY_WAIT = 2 +CONNECTION_TIMEOUT = 10 + @attr.s(slots=True, frozen=True) class ChromecastInfo: @@ -73,7 +78,8 @@ class ChromecastInfo: port = attr.ib(type=int) uuid = attr.ib(type=Optional[str], converter=attr.converters.optional(str), default=None) # always convert UUID to string if not None - model_name = attr.ib(type=str, default='') # needed for cast type + manufacturer = attr.ib(type=str, default='') + model_name = attr.ib(type=str, default='') friendly_name = attr.ib(type=Optional[str], default=None) @property @@ -111,6 +117,7 @@ def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: host=info.host, port=info.port, uuid=(info.uuid or http_device_status.uuid), friendly_name=(info.friendly_name or http_device_status.friendly_name), + manufacturer=(info.manufacturer or http_device_status.manufacturer), model_name=(info.model_name or http_device_status.model_name) ) @@ -148,7 +155,13 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: def internal_callback(name): """Handle zeroconf discovery of a new chromecast.""" mdns = listener.services[name] - _discover_chromecast(hass, ChromecastInfo(*mdns)) + _discover_chromecast(hass, ChromecastInfo( + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + )) _LOGGER.debug("Starting internal pychromecast discovery.") listener, browser = pychromecast.start_discovery(internal_callback) @@ -360,12 +373,13 @@ class CastDevice(MediaPlayerDevice): 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( - pychromecast._get_chromecast_from_host, attr.astuple(cast_info)) + pychromecast._get_chromecast_from_host, ( + cast_info.host, cast_info.port, cast_info.uuid, + cast_info.model_name, cast_info.friendly_name + ), CONNECTION_RETRY, CONNECTION_RETRY_WAIT, CONNECTION_TIMEOUT) self._chromecast = chromecast self._status_listener = CastStatusListener(self, chromecast) # Initialise connection status as connected because we can only @@ -494,6 +508,23 @@ class CastDevice(MediaPlayerDevice): """Return the name of the device.""" return self._cast_info.friendly_name + @property + def device_info(self): + """Return information about the device.""" + cast_info = self._cast_info + + if cast_info.model_name == "Google Cast Group": + return None + + return { + 'name': cast_info.friendly_name, + 'identifiers': { + (CAST_DOMAIN, cast_info.uuid.replace('-', '')) + }, + 'model': cast_info.model_name, + 'manufacturer': cast_info.manufacturer, + } + @property def state(self): """Return the state of the player.""" 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..efe04c7005b 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.0.0'] _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..cc966c0d263 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -5,23 +5,22 @@ 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 @@ -29,13 +28,11 @@ REQUIREMENTS = ['samsungctl[websocket]==0.7.1', 'wakeonlan==1.0.0'] _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 c1bfbbe59cd..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.7'] +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 c4309519e36..72ac0a046a3 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -9,8 +9,8 @@ import datetime import functools as ft import logging import socket -import urllib import threading +import urllib import voluptuous as vol @@ -388,6 +388,18 @@ class SonosDevice(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def device_info(self): + """Return information about the device.""" + return { + 'identifiers': { + (SONOS_DOMAIN, self._unique_id) + }, + 'name': self._name, + 'model': self._model.replace("Sonos ", ""), + 'manufacturer': 'Sonos', + } + @property @soco_coordinator def state(self): @@ -872,6 +884,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 diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index 4e26af9dcc2..a16658501cb 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, }) 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..d78619a8279 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -14,6 +14,7 @@ from typing import Dict # noqa: F401 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 +25,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 +34,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..57e9a83d201 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "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", + "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/fr.json b/homeassistant/components/mqtt/.translations/fr.json new file mode 100644 index 00000000000..1870c598e3b --- /dev/null +++ b/homeassistant/components/mqtt/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "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", + "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/ko.json b/homeassistant/components/mqtt/.translations/ko.json new file mode 100644 index 00000000000..a38b00fd68d --- /dev/null +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 MQTT \ube0c\ub85c\ucee4\ub9cc \uad6c\uc131\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc74c" + }, + "step": { + "broker": { + "data": { + "broker": "\ube0c\ub85c\ucee4", + "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\ud558\uc138\uc694.", + "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 19bacbc8d4c..6bb08d7e8e5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -13,7 +13,6 @@ import os import socket import time import ssl -import re import requests.certs import attr @@ -550,6 +549,7 @@ class MQTT: This method must be run in the event loop and returns a coroutine. """ async with self._paho_lock: + _LOGGER.debug("Transmitting message on %s: %s", topic, payload) await self.hass.async_add_job( self._mqttc.publish, topic, payload, qos, retain) @@ -726,23 +726,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): diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 5fc365342ae..45529411ed5 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,7 @@ 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'] DEPENDENCIES = ['http'] # None allows custom config to be created through generate_config @@ -85,6 +85,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/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/fr.json b/homeassistant/components/nest/.translations/fr.json index 734e82dbcd0..822ec6ae836 100644 --- a/homeassistant/components/nest/.translations/fr.json +++ b/homeassistant/components/nest/.translations/fr.json @@ -1,20 +1,30 @@ { "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" + "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" } }, diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index c25b57fbd62..59b0a64f6e9 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -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/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/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..1f81ac30f53 --- /dev/null +++ b/homeassistant/components/openuv/.translations/de.json @@ -0,0 +1,19 @@ +{ + "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": "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/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/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/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/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/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..bfd90b4a574 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][CONF_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..55ee566268e --- /dev/null +++ b/homeassistant/components/openuv/config_flow.py @@ -0,0 +1,73 @@ +"""Config flow to configure the OpenUV component.""" + +from collections import OrderedDict + +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +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(data_entry_flow.FlowHandler): + """Handle an OpenUV config flow.""" + + VERSION = 1 + + 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/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f3d8e269a42..47d6e181c8f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.10'] +REQUIREMENTS = ['sqlalchemy==1.2.11'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 207f2f53a7f..7b257e223db 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -169,7 +169,7 @@ def _add_columns(engine, table_name, columns_def): if 'duplicate' not in str(err).lower(): raise - _LOGGER.warning('Column %s already exists on %s, continueing', + _LOGGER.warning('Column %s already exists on %s, continuing', column_def.split(' ')[1], table_name) 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/deconz.py b/homeassistant/components/scene/deconz.py index dde78dadc49..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): @@ -38,6 +45,10 @@ class DeconzScene(Scene): """Subscribe to sensors events.""" self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._scene.deconz_id + async def async_will_remove_from_hass(self) -> None: + """Disconnect scene object when removed.""" + self._scene = None + async def async_activate(self): """Activate the scene.""" await self._scene.async_set_state({}) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index a45f8ba8930..247ac07283e 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -63,11 +63,11 @@ def is_on(hass, entity_id): @bind_hass -def turn_on(hass, entity_id, variables=None): +def turn_on(hass, entity_id, variables=None, context=None): """Turn script on.""" _, object_id = split_entity_id(entity_id) - hass.services.call(DOMAIN, object_id, variables) + hass.services.call(DOMAIN, object_id, variables, context=context) @bind_hass @@ -97,45 +97,41 @@ def async_reload(hass): 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 +145,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.