Merge pull request #16666 from home-assistant/rc

0.78.0
This commit is contained in:
Paulus Schoutsen 2018-09-17 19:03:37 +02:00 committed by GitHub
commit 9e59fc5d05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
269 changed files with 5659 additions and 2095 deletions

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ from logging import getLogger
from typing import Any, Dict, List, Optional # noqa: F401
import hmac
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util
@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ be in JSON as it's more readable.
Exchange the authorization code retrieved from the login flow for tokens.
{
"client_id": "https://hassbian.local:8123/",
"grant_type": "authorization_code",
"code": "411ee2f916e648d691e937ae9344681e"
}
@ -32,6 +33,7 @@ token.
Request a new access token using a refresh token.
{
"client_id": "https://hassbian.local:8123/",
"grant_type": "refresh_token",
"refresh_token": "IJKLMNOPQRST"
}
@ -55,6 +57,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']))

View File

@ -66,7 +66,7 @@ associate with an credential if "type" set to "link_user" in
"version": 1
}
"""
import aiohttp.web
from aiohttp import web
import voluptuous as vol
from homeassistant import data_entry_flow
@ -95,11 +95,20 @@ class AuthProvidersView(HomeAssistantView):
async def get(self, request):
"""Get available auth providers."""
hass = request.app['hass']
if not hass.components.onboarding.async_is_onboarded():
return self.json_message(
message='Onboarding not finished',
status_code=400,
message_code='onboarding_required'
)
return self.json([{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in request.app['hass'].auth.auth_providers])
} for provider in hass.auth.auth_providers])
def _prepare_result_json(result):
@ -139,7 +148,7 @@ class LoginFlowIndexView(HomeAssistantView):
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
return web.Response(status=405)
@RequestDataValidator(vol.Schema({
vol.Required('client_id'): str,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,13 @@
"config": {
"error": {
"invalid_pin": "Ugyldig PIN, pr\u00f8v igen."
},
"step": {
"init": {
"data": {
"pin": "Pin kode (valgfri)"
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -24,6 +24,6 @@
"title": "Hub de liaison"
}
},
"title": "Pont Philips Hue"
"title": "Philips Hue"
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
{
"config": {
"step": {
"confirm": {
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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