mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
commit
9e59fc5d05
16
.coveragerc
16
.coveragerc
@ -116,11 +116,14 @@ omit =
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/habitica/*
|
||||
homeassistant/components/*/habitica.py
|
||||
|
||||
homeassistant/components/hangouts/__init__.py
|
||||
homeassistant/components/hangouts/const.py
|
||||
homeassistant/components/hangouts/hangouts_bot.py
|
||||
homeassistant/components/hangouts/hangups_utils.py
|
||||
homeassistant/components/*/hangouts.py
|
||||
homeassistant/components/*/hangouts.py
|
||||
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/*/hdmi_cec.py
|
||||
@ -142,12 +145,12 @@ omit =
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
|
||||
homeassistant/components/insteon/*
|
||||
homeassistant/components/*/insteon.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
|
||||
|
||||
homeassistant/components/insteon_plm.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
@ -225,7 +228,7 @@ omit =
|
||||
homeassistant/components/opencv.py
|
||||
homeassistant/components/*/opencv.py
|
||||
|
||||
homeassistant/components/openuv.py
|
||||
homeassistant/components/openuv/__init__.py
|
||||
homeassistant/components/*/openuv.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
@ -374,6 +377,7 @@ omit =
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
|
||||
homeassistant/components/apiai.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
@ -411,6 +415,7 @@ omit =
|
||||
homeassistant/components/climate/honeywell.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/opentherm_gw.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/climate/sensibo.py
|
||||
@ -759,6 +764,7 @@ omit =
|
||||
homeassistant/components/sensor/uscis.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/viaggiatreno.py
|
||||
homeassistant/components/sensor/volkszaehler.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/waze_travel_time.py
|
||||
homeassistant/components/sensor/whois.py
|
||||
@ -789,6 +795,8 @@ omit =
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/snmp.py
|
||||
homeassistant/components/switch/switchbot.py
|
||||
homeassistant/components/switch/switchmate.py
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
|
331
LICENSE.md
331
LICENSE.md
@ -1,194 +1,201 @@
|
||||
Apache License
|
||||
==============
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
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.
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
98
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
Executable file
98
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
Executable 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()
|
@ -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:
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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": {
|
||||
|
16
homeassistant/components/auth/.translations/sv.json
Normal file
16
homeassistant/components/auth/.translations/sv.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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']))
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
189
homeassistant/components/climate/opentherm_gw.py
Normal file
189
homeassistant/components/climate/opentherm_gw.py
Normal 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
|
@ -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
|
||||
|
@ -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."""
|
||||
|
73
homeassistant/components/cover/insteon.py
Normal file
73
homeassistant/components/cover/insteon.py
Normal 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)
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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',
|
||||
|
68
homeassistant/components/geo_location/__init__.py
Normal file
68
homeassistant/components/geo_location/__init__.py
Normal 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
|
132
homeassistant/components/geo_location/demo.py
Normal file
132
homeassistant/components/geo_location/demo.py
Normal 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
|
158
homeassistant/components/habitica/__init__.py
Normal file
158
homeassistant/components/habitica/__init__.py
Normal 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
|
15
homeassistant/components/habitica/services.yaml
Normal file
15
homeassistant/components/habitica/services.yaml
Normal 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"}'
|
@ -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": {
|
||||
|
29
homeassistant/components/hangouts/.translations/sv.json
Normal file
29
homeassistant/components/hangouts/.translations/sv.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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',
|
||||
|
@ -2,6 +2,13 @@
|
||||
"config": {
|
||||
"error": {
|
||||
"invalid_pin": "Ugyldig PIN, pr\u00f8v igen."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"pin": "Pin kode (valgfri)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 ",
|
||||
"title": "Lier le point d'acc\u00e8s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "HomematicIP Cloud"
|
||||
}
|
||||
}
|
@ -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."
|
||||
},
|
||||
|
@ -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": {
|
||||
|
@ -24,6 +24,6 @@
|
||||
"title": "Hub de liaison"
|
||||
}
|
||||
},
|
||||
"title": "Pont Philips Hue"
|
||||
"title": "Philips Hue"
|
||||
}
|
||||
}
|
@ -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):
|
||||
|
@ -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
|
||||
|
14
homeassistant/components/ios/.translations/ca.json
Normal file
14
homeassistant/components/ios/.translations/ca.json
Normal 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"
|
||||
}
|
||||
}
|
10
homeassistant/components/ios/.translations/cs.json
Normal file
10
homeassistant/components/ios/.translations/cs.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Home Assistant iOS"
|
||||
}
|
||||
},
|
||||
"title": "Home Assistant iOS"
|
||||
}
|
||||
}
|
14
homeassistant/components/ios/.translations/fr.json
Normal file
14
homeassistant/components/ios/.translations/fr.json
Normal 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"
|
||||
}
|
||||
}
|
14
homeassistant/components/ios/.translations/ko.json
Normal file
14
homeassistant/components/ios/.translations/ko.json
Normal 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"
|
||||
}
|
||||
}
|
14
homeassistant/components/ios/.translations/nl.json
Normal file
14
homeassistant/components/ios/.translations/nl.json
Normal 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"
|
||||
}
|
||||
}
|
14
homeassistant/components/ios/.translations/pl.json
Normal file
14
homeassistant/components/ios/.translations/pl.json
Normal 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"
|
||||
}
|
||||
}
|
14
homeassistant/components/ios/.translations/zh-Hans.json
Normal file
14
homeassistant/components/ios/.translations/zh-Hans.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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."""
|
||||
|
@ -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}
|
||||
|
@ -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."""
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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']
|
||||
|
@ -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(),
|
||||
])
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 | \
|
||||
|
@ -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."""
|
||||
|
@ -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()
|
||||
|
@ -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']
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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]:
|
||||
|
@ -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__)
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user