mirror of
https://github.com/home-assistant/core.git
synced 2026-01-05 07:15:25 +01:00
Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dc29bd2c3 | ||
|
|
20c316bce4 | ||
|
|
8b475f45e9 | ||
|
|
a4318682f7 | ||
|
|
a14d8057ed | ||
|
|
55f8b0a2f5 | ||
|
|
bb37300a48 | ||
|
|
0f12b37977 | ||
|
|
7f18739267 | ||
|
|
a1b478b3ac | ||
|
|
edf1f44668 | ||
|
|
60f780cc37 | ||
|
|
7d0cc7e26c | ||
|
|
864a254071 | ||
|
|
5995c6a2ac | ||
|
|
ed0cfc4f31 | ||
|
|
6db069881b | ||
|
|
ca4f69f557 | ||
|
|
37ccf87516 | ||
|
|
201c9fed77 | ||
|
|
3b5775573b | ||
|
|
6e22a0e4d9 | ||
|
|
ce5b4cd51e | ||
|
|
538236de8f | ||
|
|
1007bb83aa | ||
|
|
79955a5785 | ||
|
|
e60f9ca392 | ||
|
|
ae581694ac | ||
|
|
70fe463ef0 | ||
|
|
84858f5c19 | ||
|
|
a6ba5ec1c8 | ||
|
|
c2fe0d0120 | ||
|
|
b6ca03ce47 | ||
|
|
23f1b49e55 | ||
|
|
6e3ec97acf | ||
|
|
4a6afc5614 | ||
|
|
b557c17f76 | ||
|
|
c587536547 | ||
|
|
4c6394b307 | ||
|
|
534233388c | ||
|
|
43b31e88ba | ||
|
|
6197fe0121 | ||
|
|
1f6331c69d | ||
|
|
fd568d77c7 | ||
|
|
f32098abe4 | ||
|
|
b65d7daed8 | ||
|
|
9ea0c409e6 | ||
|
|
2ee62b10bc | ||
|
|
dbdd0a1f56 | ||
|
|
df8c59406b | ||
|
|
c5a2ffbcb9 | ||
|
|
e62bb299ff | ||
|
|
6ee8d9bd65 | ||
|
|
14a34f8c4b | ||
|
|
3b93fa80be | ||
|
|
57977bcef3 | ||
|
|
0d4841cbea | ||
|
|
f7d7d825b0 | ||
|
|
1d1408b98d | ||
|
|
b9eb0081cd | ||
|
|
287b1bce15 | ||
|
|
ec3d2e97e8 | ||
|
|
1ff329d9d6 | ||
|
|
703d71c064 | ||
|
|
a2a4c633f3 | ||
|
|
e6dd4f6e13 | ||
|
|
b327ea2023 | ||
|
|
b333dba875 | ||
|
|
02238b6412 | ||
|
|
bd62248841 | ||
|
|
dabbd7bd63 | ||
|
|
b5c7afcf75 | ||
|
|
f8f8da959a | ||
|
|
9970965718 | ||
|
|
a1d8b0e9b3 | ||
|
|
1e7cfc04af | ||
|
|
0f1bcfd63b | ||
|
|
f65c3940ae | ||
|
|
46de89e1a3 | ||
|
|
852526e10a | ||
|
|
91d6d0df84 | ||
|
|
cb129bd207 | ||
|
|
a6e9dc81aa | ||
|
|
5f7ac09a74 | ||
|
|
42775142f8 | ||
|
|
2525fc52b3 | ||
|
|
07dde62e70 | ||
|
|
cb458b7745 | ||
|
|
b2df199674 | ||
|
|
857c58c4b7 | ||
|
|
b82371f44b | ||
|
|
1c525968d1 | ||
|
|
5ec61e4649 | ||
|
|
184d0a99c0 | ||
|
|
232f56de62 | ||
|
|
66e33c7979 | ||
|
|
6420ab5535 | ||
|
|
ed3fe1cc6f | ||
|
|
cd1cfd7e8e | ||
|
|
31e23ebae2 | ||
|
|
fb65276daf | ||
|
|
bedd2d7e41 | ||
|
|
120111ceee | ||
|
|
e6390b8e41 | ||
|
|
d7fd9247a9 | ||
|
|
0dc155c4d3 | ||
|
|
0feb4c5439 | ||
|
|
f3588a8782 | ||
|
|
2145ac5e46 | ||
|
|
c39e6b9618 | ||
|
|
855cbc0aed | ||
|
|
00c366d7ea | ||
|
|
3c3a53a137 | ||
|
|
dd59054003 | ||
|
|
36f566a529 | ||
|
|
4d93a9fd38 | ||
|
|
d3df96a8de | ||
|
|
6c77702dcc | ||
|
|
86165750ff | ||
|
|
63b28aa39d | ||
|
|
279fd39677 | ||
|
|
c978281d1e | ||
|
|
311a44007c | ||
|
|
47401739ea | ||
|
|
11ba7cc8ce | ||
|
|
c3ad30ec87 | ||
|
|
a64a66dd62 | ||
|
|
dffe36761d | ||
|
|
0a186650bf | ||
|
|
6c77c9d372 | ||
|
|
4a4b9180d8 | ||
|
|
5d6db9a915 | ||
|
|
235282e335 | ||
|
|
6f582dcf24 | ||
|
|
9db8759317 | ||
|
|
136cc1d44d | ||
|
|
4c258ce08b | ||
|
|
3c04b0756f | ||
|
|
c0229ebb77 | ||
|
|
cfe7c0aa01 | ||
|
|
f874efb224 | ||
|
|
3da4642194 | ||
|
|
0aad056ca7 | ||
|
|
c5ceb40598 | ||
|
|
27a37e2013 | ||
|
|
10d1e81f10 | ||
|
|
56bbadb501 | ||
|
|
fa79aead9a | ||
|
|
24fec3e826 | ||
|
|
2524dca7bf | ||
|
|
56f17b8651 | ||
|
|
49623d2dad | ||
|
|
66479dc2e5 | ||
|
|
bbbec5a056 | ||
|
|
94b55efef3 | ||
|
|
fd38caa287 | ||
|
|
c61a652c90 | ||
|
|
e3e014bccc | ||
|
|
26590e244c | ||
|
|
39971ee919 | ||
|
|
2205090795 | ||
|
|
a277470363 | ||
|
|
19f2bbf52f | ||
|
|
dbb786c548 | ||
|
|
4fbe3bb070 | ||
|
|
9066ac44fe | ||
|
|
742144f401 | ||
|
|
c0b6a857f7 | ||
|
|
d6dee62c92 | ||
|
|
41017f10a3 | ||
|
|
ba50a5c329 | ||
|
|
4208bb457d | ||
|
|
15af6b1ad9 | ||
|
|
3921dc77a6 | ||
|
|
0094fd5c34 | ||
|
|
d58e401812 | ||
|
|
c79c94550f | ||
|
|
9b950f5192 | ||
|
|
2520fddbdf | ||
|
|
3f21966ec9 | ||
|
|
69502163bd | ||
|
|
893e0f8db6 | ||
|
|
1c8b52f630 | ||
|
|
6e4fb7a937 | ||
|
|
ab1939f56f | ||
|
|
15507df407 | ||
|
|
46ea28a4f8 | ||
|
|
c8458fd7c5 | ||
|
|
e681a7929c | ||
|
|
b2d37ccef6 | ||
|
|
9dd2c36de4 | ||
|
|
b92350fb55 | ||
|
|
6c0fc65eaf | ||
|
|
42ba2a68ce | ||
|
|
508d0459a7 | ||
|
|
dbae410cf4 | ||
|
|
ae51dc08bf | ||
|
|
672a3c7178 | ||
|
|
f8bc3411ad | ||
|
|
038168c417 | ||
|
|
73034c933e | ||
|
|
d3ceb9080c | ||
|
|
3893d8a876 | ||
|
|
05924a2868 | ||
|
|
021d08a9c4 | ||
|
|
5a71a22fb9 | ||
|
|
6064932e2e | ||
|
|
9de7034d0e | ||
|
|
96d5684a89 | ||
|
|
91962e2681 | ||
|
|
ee31f89049 | ||
|
|
370c3f28b8 | ||
|
|
66110a7d57 | ||
|
|
c419cbb46f | ||
|
|
a02d7989d5 | ||
|
|
7325847fa9 | ||
|
|
124495dd84 | ||
|
|
0c01f3a0fe | ||
|
|
0ea2d99910 | ||
|
|
6456f66b47 | ||
|
|
94eee6d069 | ||
|
|
6e5a2a77ab | ||
|
|
35b609dd8b | ||
|
|
0df99f8762 | ||
|
|
6781ecf159 | ||
|
|
a4b843eb2d | ||
|
|
302717e8a1 | ||
|
|
617647c5fd | ||
|
|
4b5d578c08 | ||
|
|
bfc55137ea | ||
|
|
e98e7e2751 | ||
|
|
b687de879c | ||
|
|
4048ad36a8 | ||
|
|
8c2f0e3b30 | ||
|
|
6cabbd2592 | ||
|
|
8d22754a06 | ||
|
|
be6d1b5e94 | ||
|
|
c84f1d7d33 | ||
|
|
49845d9398 | ||
|
|
895306f822 | ||
|
|
a729742757 | ||
|
|
6bc03ee763 | ||
|
|
75580dfade | ||
|
|
1f8699d9b4 | ||
|
|
fca5d55b43 | ||
|
|
659616a4eb | ||
|
|
3b4f7b4f5d | ||
|
|
62432ced90 | ||
|
|
27873b4457 | ||
|
|
1e7333eeb6 | ||
|
|
7cd620d30f | ||
|
|
7a180ac205 | ||
|
|
153ccda853 | ||
|
|
067e4f6d9a | ||
|
|
9d6ce609f9 | ||
|
|
9800b74a6d | ||
|
|
ef5b2a2492 | ||
|
|
60179a1cbb | ||
|
|
e0cea2d18d | ||
|
|
e29dfa8609 | ||
|
|
ef39bca52e | ||
|
|
5a3ea74a26 | ||
|
|
8869617890 | ||
|
|
62f970e486 | ||
|
|
f9a21dbfda | ||
|
|
86c6b4d8e3 | ||
|
|
7bfa81c592 | ||
|
|
d07e40c483 | ||
|
|
0e7e58f172 | ||
|
|
cbdfc95cc8 | ||
|
|
1642502a70 | ||
|
|
33ebd99068 | ||
|
|
9c17e95fc5 | ||
|
|
1533bc1e1f | ||
|
|
da3695dccc | ||
|
|
40c8f5f70e | ||
|
|
3ceee66e1b | ||
|
|
6b908b6f4e | ||
|
|
a74b081d44 | ||
|
|
bc8093c73b | ||
|
|
ca2712506b | ||
|
|
c871e8da5d | ||
|
|
722c27f1e2 | ||
|
|
e3fcf46566 | ||
|
|
1117371b31 | ||
|
|
addca54118 | ||
|
|
3db5d5bbf9 | ||
|
|
1375adfeab | ||
|
|
00cbdffa12 | ||
|
|
c5f012c85a | ||
|
|
656eae288e | ||
|
|
5898307715 | ||
|
|
9b0efdc8c8 | ||
|
|
33990badcd | ||
|
|
8061f15aec | ||
|
|
25f7c31911 | ||
|
|
bb98331ba4 | ||
|
|
07d139b3a8 | ||
|
|
f4ef8fd1bc | ||
|
|
ba836c2e36 | ||
|
|
a0ab356936 | ||
|
|
734a83c657 | ||
|
|
b42f4012d1 | ||
|
|
8501312292 | ||
|
|
3faed2edc1 | ||
|
|
bc70619b17 |
@@ -64,6 +64,8 @@ omit =
|
||||
homeassistant/components/cast/*
|
||||
homeassistant/components/*/cast.py
|
||||
|
||||
homeassistant/components/cloudflare.py
|
||||
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
@@ -192,7 +194,7 @@ omit =
|
||||
homeassistant/components/mychevy.py
|
||||
homeassistant/components/*/mychevy.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/mysensors/*
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
@@ -341,6 +343,9 @@ omit =
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/tuya.py
|
||||
homeassistant/components/*/tuya.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
@@ -612,6 +617,7 @@ omit =
|
||||
homeassistant/components/sensor/domain_expiry.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/duke_energy.py
|
||||
homeassistant/components/sensor/dwd_weather_warnings.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -107,3 +107,6 @@ desktop.ini
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
|
||||
# monkeytype
|
||||
monkeytype.sqlite3
|
||||
|
||||
2
.isort.cfg
Normal file
2
.isort.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
multi_line_output=4
|
||||
16
.travis.yml
16
.travis.yml
@@ -16,11 +16,17 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
# - python: "3.6-dev"
|
||||
# env: TOXENV=py36
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.7"
|
||||
env: TOXENV=py37
|
||||
dist: xenial
|
||||
- python: "3.8-dev"
|
||||
env: TOXENV=py38
|
||||
dist: xenial
|
||||
if: branch = dev AND type = push
|
||||
allow_failures:
|
||||
- python: "3.8-dev"
|
||||
env: TOXENV=py38
|
||||
dist: xenial
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
||||
@@ -241,7 +241,7 @@ def cmdline() -> List[str]:
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> Optional[int]:
|
||||
args: argparse.Namespace) -> int:
|
||||
"""Set up HASS and run."""
|
||||
from homeassistant import bootstrap
|
||||
|
||||
@@ -274,7 +274,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
log_no_color=args.log_no_color)
|
||||
|
||||
if hass is None:
|
||||
return None
|
||||
return -1
|
||||
|
||||
if args.open_ui:
|
||||
# Imported here to avoid importing asyncio before monkey patch
|
||||
|
||||
@@ -1,503 +0,0 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
import asyncio
|
||||
import binascii
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import importlib
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUTH_PROVIDERS = Registry()
|
||||
|
||||
AUTH_PROVIDER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): str,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
# Specify ID if you have two auth providers for same type.
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
DATA_REQS = 'auth_reqs_processed'
|
||||
|
||||
|
||||
def generate_secret(entropy: int = 32) -> str:
|
||||
"""Generate a secret.
|
||||
|
||||
Backport of secrets.token_hex from Python 3.6
|
||||
|
||||
Event loop friendly.
|
||||
"""
|
||||
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
|
||||
|
||||
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||
|
||||
initialized = False
|
||||
|
||||
def __init__(self, hass, store, config):
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
"""
|
||||
return self.config.get(CONF_ID)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Return type of the provider."""
|
||||
return self.config[CONF_TYPE]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the auth provider."""
|
||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||
|
||||
async def async_credentials(self):
|
||||
"""Return all credentials of this provider."""
|
||||
return await self.store.credentials_for_provider(self.type, self.id)
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data):
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type,
|
||||
auth_provider_id=self.id,
|
||||
data=data,
|
||||
)
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Initialize the auth provider.
|
||||
|
||||
Optional.
|
||||
"""
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return the data flow for logging in with auth provider."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class User:
|
||||
"""A user."""
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
name = attr.ib(type=str, default=None)
|
||||
# For persisting and see if saved?
|
||||
# store = attr.ib(type=AuthStore, default=None)
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict))
|
||||
|
||||
def as_dict(self):
|
||||
"""Convert user object to a dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'is_owner': self.is_owner,
|
||||
'is_active': self.is_active,
|
||||
'name': self.name,
|
||||
}
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class RefreshToken:
|
||||
"""RefreshToken for a user to grant new access tokens."""
|
||||
|
||||
user = attr.ib(type=User)
|
||||
client_id = attr.ib(type=str)
|
||||
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)))
|
||||
access_tokens = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class AccessToken:
|
||||
"""Access token to access the API.
|
||||
|
||||
These will only ever be stored in memory and not be persisted.
|
||||
"""
|
||||
|
||||
refresh_token = attr.ib(type=RefreshToken)
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(generate_secret))
|
||||
|
||||
@property
|
||||
def expires(self):
|
||||
"""Return datetime when this token expires."""
|
||||
return self.created_at + self.refresh_token.access_token_expiration
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
# Allow the auth provider to store data to represent their auth.
|
||||
data = attr.ib(type=dict)
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_new = attr.ib(type=bool, default=True)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Client:
|
||||
"""Client that interacts with Home Assistant on behalf of a user."""
|
||||
|
||||
name = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
|
||||
redirect_uris = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
|
||||
async def load_auth_provider_module(hass, provider):
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
'homeassistant.auth_providers.{}'.format(provider))
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find auth provider %s', provider)
|
||||
return None
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
||||
if processed is None:
|
||||
processed = hass.data[DATA_REQS] = set()
|
||||
elif provider in processed:
|
||||
return module
|
||||
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
|
||||
return module
|
||||
|
||||
|
||||
async def auth_manager_from_config(hass, provider_configs):
|
||||
"""Initialize an auth manager from config."""
|
||||
store = AuthStore(hass)
|
||||
if provider_configs:
|
||||
providers = await asyncio.gather(
|
||||
*[_auth_provider_from_config(hass, store, config)
|
||||
for config in provider_configs])
|
||||
else:
|
||||
providers = []
|
||||
# So returned auth providers are in same order as config
|
||||
provider_hash = OrderedDict()
|
||||
for provider in providers:
|
||||
if provider is None:
|
||||
continue
|
||||
|
||||
key = (provider.type, provider.id)
|
||||
|
||||
if key in provider_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate provider: %s. Please add unique IDs if you '
|
||||
'want to have the same provider twice.', key)
|
||||
continue
|
||||
|
||||
provider_hash[key] = provider
|
||||
manager = AuthManager(hass, store, provider_hash)
|
||||
return manager
|
||||
|
||||
|
||||
async def _auth_provider_from_config(hass, store, config):
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config)
|
||||
|
||||
|
||||
class AuthManager:
|
||||
"""Manage the authentication for Home Assistant."""
|
||||
|
||||
def __init__(self, hass, store, providers):
|
||||
"""Initialize the auth manager."""
|
||||
self._store = store
|
||||
self._providers = providers
|
||||
self.login_flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_login_flow,
|
||||
self._async_finish_login_flow)
|
||||
self.access_tokens = {}
|
||||
|
||||
@property
|
||||
def async_auth_providers(self):
|
||||
"""Return a list of available auth providers."""
|
||||
return self._providers.values()
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_get_or_create_user(self, credentials):
|
||||
"""Get or create a user."""
|
||||
return await self._store.async_get_or_create_user(
|
||||
credentials, self._async_get_auth_provider(credentials))
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Link credentials to an existing user."""
|
||||
await self._store.async_link_user(user, credentials)
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
await self._store.async_remove_user(user)
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id):
|
||||
"""Create a new refresh token for a user."""
|
||||
return await self._store.async_create_refresh_token(user, client_id)
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
return await self._store.async_get_refresh_token(token)
|
||||
|
||||
@callback
|
||||
def async_create_access_token(self, refresh_token):
|
||||
"""Create a new access token."""
|
||||
access_token = AccessToken(refresh_token)
|
||||
self.access_tokens[access_token.token] = access_token
|
||||
return access_token
|
||||
|
||||
@callback
|
||||
def async_get_access_token(self, token):
|
||||
"""Get an access token."""
|
||||
return self.access_tokens.get(token)
|
||||
|
||||
async def async_create_client(self, name, *, redirect_uris=None,
|
||||
no_secret=False):
|
||||
"""Create a new client."""
|
||||
return await self._store.async_create_client(
|
||||
name, redirect_uris, no_secret)
|
||||
|
||||
async def async_get_client(self, client_id):
|
||||
"""Get a client."""
|
||||
return await self._store.async_get_client(client_id)
|
||||
|
||||
async def _async_create_login_flow(self, handler, *, source, data):
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
if not auth_provider.initialized:
|
||||
auth_provider.initialized = True
|
||||
await auth_provider.async_initialize()
|
||||
|
||||
return await auth_provider.async_credential_flow()
|
||||
|
||||
async def _async_finish_login_flow(self, result):
|
||||
"""Result of a credential login flow."""
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return None
|
||||
|
||||
auth_provider = self._providers[result['handler']]
|
||||
return await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
|
||||
@callback
|
||||
def _async_get_auth_provider(self, credentials):
|
||||
"""Helper to get auth provider from a set of credentials."""
|
||||
auth_provider_key = (credentials.auth_provider_type,
|
||||
credentials.auth_provider_id)
|
||||
return self._providers[auth_provider_key]
|
||||
|
||||
|
||||
class AuthStore:
|
||||
"""Stores authentication info.
|
||||
|
||||
Any mutation to an object should happen inside the auth store.
|
||||
|
||||
The auth store is lazy. It won't load the data from disk until a method is
|
||||
called that needs it.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self.users = None
|
||||
self.clients = None
|
||||
self._load_lock = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
async def credentials_for_provider(self, provider_type, provider_id):
|
||||
"""Return credentials for specific auth provider type and id."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
return [
|
||||
credentials
|
||||
for user in self.users.values()
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == provider_type and
|
||||
credentials.auth_provider_id == provider_id)
|
||||
]
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
return self.users.get(user_id)
|
||||
|
||||
async def async_get_or_create_user(self, credentials, auth_provider):
|
||||
"""Get or create a new user for given credentials.
|
||||
|
||||
If link_user is passed in, the credentials will be linked to the passed
|
||||
in user if the credentials are new.
|
||||
"""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
# New credentials, store in user
|
||||
if credentials.is_new:
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
# Make owner and activate user if it's the first user.
|
||||
if self.users:
|
||||
is_owner = False
|
||||
is_active = False
|
||||
else:
|
||||
is_owner = True
|
||||
is_active = True
|
||||
|
||||
new_user = User(
|
||||
is_owner=is_owner,
|
||||
is_active=is_active,
|
||||
name=info.get('name'),
|
||||
)
|
||||
self.users[new_user.id] = new_user
|
||||
await self.async_link_user(new_user, credentials)
|
||||
return new_user
|
||||
|
||||
for user in self.users.values():
|
||||
for creds in user.credentials:
|
||||
if (creds.auth_provider_type == credentials.auth_provider_type
|
||||
and creds.auth_provider_id ==
|
||||
credentials.auth_provider_id):
|
||||
return user
|
||||
|
||||
raise ValueError('We got credentials with ID but found no user')
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Add credentials to an existing user."""
|
||||
user.credentials.append(credentials)
|
||||
await self.async_save()
|
||||
credentials.is_new = False
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
self.users.pop(user.id)
|
||||
await self.async_save()
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id):
|
||||
"""Create a new token for a user."""
|
||||
refresh_token = RefreshToken(user, client_id)
|
||||
user.refresh_tokens[refresh_token.token] = refresh_token
|
||||
await self.async_save()
|
||||
return refresh_token
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
for user in self.users.values():
|
||||
refresh_token = user.refresh_tokens.get(token)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
|
||||
return None
|
||||
|
||||
async def async_create_client(self, name, redirect_uris, no_secret):
|
||||
"""Create a new client."""
|
||||
if self.clients is None:
|
||||
await self.async_load()
|
||||
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'redirect_uris': redirect_uris
|
||||
}
|
||||
|
||||
if no_secret:
|
||||
kwargs['secret'] = None
|
||||
|
||||
client = Client(**kwargs)
|
||||
self.clients[client.id] = client
|
||||
await self.async_save()
|
||||
return client
|
||||
|
||||
async def async_get_client(self, client_id):
|
||||
"""Get a client."""
|
||||
if self.clients is None:
|
||||
await self.async_load()
|
||||
|
||||
return self.clients.get(client_id)
|
||||
|
||||
async def async_load(self):
|
||||
"""Load the users."""
|
||||
async with self._load_lock:
|
||||
self.users = {}
|
||||
self.clients = {}
|
||||
|
||||
async def async_save(self):
|
||||
"""Save users."""
|
||||
pass
|
||||
242
homeassistant/auth/__init__.py
Normal file
242
homeassistant/auth/__init__.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import models
|
||||
from . import auth_store
|
||||
from .providers import auth_provider_from_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def auth_manager_from_config(hass, provider_configs):
|
||||
"""Initialize an auth manager from config."""
|
||||
store = auth_store.AuthStore(hass)
|
||||
if provider_configs:
|
||||
providers = await asyncio.gather(
|
||||
*[auth_provider_from_config(hass, store, config)
|
||||
for config in provider_configs])
|
||||
else:
|
||||
providers = []
|
||||
# So returned auth providers are in same order as config
|
||||
provider_hash = OrderedDict()
|
||||
for provider in providers:
|
||||
if provider is None:
|
||||
continue
|
||||
|
||||
key = (provider.type, provider.id)
|
||||
|
||||
if key in provider_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate provider: %s. Please add unique IDs if you '
|
||||
'want to have the same provider twice.', key)
|
||||
continue
|
||||
|
||||
provider_hash[key] = provider
|
||||
manager = AuthManager(hass, store, provider_hash)
|
||||
return manager
|
||||
|
||||
|
||||
class AuthManager:
|
||||
"""Manage the authentication for Home Assistant."""
|
||||
|
||||
def __init__(self, hass, store, providers):
|
||||
"""Initialize the auth manager."""
|
||||
self._store = store
|
||||
self._providers = providers
|
||||
self.login_flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_login_flow,
|
||||
self._async_finish_login_flow)
|
||||
self._access_tokens = OrderedDict()
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
"""Return if any auth providers are registered."""
|
||||
return bool(self._providers)
|
||||
|
||||
@property
|
||||
def support_legacy(self):
|
||||
"""
|
||||
Return if legacy_api_password auth providers are registered.
|
||||
|
||||
Should be removed when we removed legacy_api_password auth providers.
|
||||
"""
|
||||
for provider_type, _ in self._providers:
|
||||
if provider_type == 'legacy_api_password':
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def auth_providers(self):
|
||||
"""Return a list of available auth providers."""
|
||||
return list(self._providers.values())
|
||||
|
||||
async def async_get_users(self):
|
||||
"""Retrieve all users."""
|
||||
return await self._store.async_get_users()
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_create_system_user(self, name):
|
||||
"""Create a system user."""
|
||||
return await self._store.async_create_user(
|
||||
name=name,
|
||||
system_generated=True,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
async def async_create_user(self, name):
|
||||
"""Create a user."""
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
}
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
kwargs['is_owner'] = True
|
||||
|
||||
return await self._store.async_create_user(**kwargs)
|
||||
|
||||
async def async_get_or_create_user(self, credentials):
|
||||
"""Get or create a user."""
|
||||
if not credentials.is_new:
|
||||
for user in await self._store.async_get_users():
|
||||
for creds in user.credentials:
|
||||
if creds.id == credentials.id:
|
||||
return user
|
||||
|
||||
raise ValueError('Unable to find the user.')
|
||||
|
||||
auth_provider = self._async_get_auth_provider(credentials)
|
||||
|
||||
if auth_provider is None:
|
||||
raise RuntimeError('Credential with unknown provider encountered')
|
||||
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
|
||||
return await self._store.async_create_user(
|
||||
credentials=credentials,
|
||||
name=info.get('name'),
|
||||
)
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Link credentials to an existing user."""
|
||||
await self._store.async_link_user(user, credentials)
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
tasks = [
|
||||
self.async_remove_credentials(credentials)
|
||||
for credentials in user.credentials
|
||||
]
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
await self._store.async_remove_user(user)
|
||||
|
||||
async def async_activate_user(self, user):
|
||||
"""Activate a user."""
|
||||
await self._store.async_activate_user(user)
|
||||
|
||||
async def async_deactivate_user(self, user):
|
||||
"""Deactivate a user."""
|
||||
if user.is_owner:
|
||||
raise ValueError('Unable to deactive the owner')
|
||||
await self._store.async_deactivate_user(user)
|
||||
|
||||
async def async_remove_credentials(self, credentials):
|
||||
"""Remove credentials."""
|
||||
provider = self._async_get_auth_provider(credentials)
|
||||
|
||||
if (provider is not None and
|
||||
hasattr(provider, 'async_will_remove_credentials')):
|
||||
await provider.async_will_remove_credentials(credentials)
|
||||
|
||||
await self._store.async_remove_credentials(credentials)
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id=None):
|
||||
"""Create a new refresh token for a user."""
|
||||
if not user.is_active:
|
||||
raise ValueError('User is not active')
|
||||
|
||||
if user.system_generated and client_id is not None:
|
||||
raise ValueError(
|
||||
'System generated users cannot have refresh tokens connected '
|
||||
'to a client.')
|
||||
|
||||
if not user.system_generated 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)
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
return await self._store.async_get_refresh_token(token)
|
||||
|
||||
@callback
|
||||
def async_create_access_token(self, refresh_token):
|
||||
"""Create a new access token."""
|
||||
access_token = models.AccessToken(refresh_token=refresh_token)
|
||||
self._access_tokens[access_token.token] = access_token
|
||||
return access_token
|
||||
|
||||
@callback
|
||||
def async_get_access_token(self, token):
|
||||
"""Get an access token."""
|
||||
tkn = self._access_tokens.get(token)
|
||||
|
||||
if tkn is None:
|
||||
_LOGGER.debug('Attempt to get non-existing access token')
|
||||
return None
|
||||
|
||||
if tkn.expired or not tkn.refresh_token.user.is_active:
|
||||
if tkn.expired:
|
||||
_LOGGER.debug('Attempt to get expired access token')
|
||||
else:
|
||||
_LOGGER.debug('Attempt to get access token for inactive user')
|
||||
self._access_tokens.pop(token)
|
||||
return None
|
||||
|
||||
return tkn
|
||||
|
||||
async def _async_create_login_flow(self, handler, *, source, data):
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
return await auth_provider.async_credential_flow()
|
||||
|
||||
async def _async_finish_login_flow(self, result):
|
||||
"""Result of a credential login flow."""
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return None
|
||||
|
||||
auth_provider = self._providers[result['handler']]
|
||||
return await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
|
||||
@callback
|
||||
def _async_get_auth_provider(self, credentials):
|
||||
"""Helper to get auth provider from a set of credentials."""
|
||||
auth_provider_key = (credentials.auth_provider_type,
|
||||
credentials.auth_provider_id)
|
||||
return self._providers.get(auth_provider_key)
|
||||
|
||||
async def _user_should_be_owner(self):
|
||||
"""Determine if user should be owner.
|
||||
|
||||
A user should be an owner if it is the first non-system user that is
|
||||
being created.
|
||||
"""
|
||||
for user in await self._store.async_get_users():
|
||||
if not user.system_generated:
|
||||
return False
|
||||
|
||||
return True
|
||||
240
homeassistant/auth/auth_store.py
Normal file
240
homeassistant/auth/auth_store.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Storage for auth models."""
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import models
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
|
||||
|
||||
class AuthStore:
|
||||
"""Stores authentication info.
|
||||
|
||||
Any mutation to an object should happen inside the auth store.
|
||||
|
||||
The auth store is lazy. It won't load the data from disk until a method is
|
||||
called that needs it.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self._users = None
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
async def async_get_users(self):
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
return list(self._users.values())
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user by id."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
return self._users.get(user_id)
|
||||
|
||||
async def async_create_user(self, name, is_owner=None, is_active=None,
|
||||
system_generated=None, credentials=None):
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
kwargs = {
|
||||
'name': name
|
||||
}
|
||||
|
||||
if is_owner is not None:
|
||||
kwargs['is_owner'] = is_owner
|
||||
|
||||
if is_active is not None:
|
||||
kwargs['is_active'] = is_active
|
||||
|
||||
if system_generated is not None:
|
||||
kwargs['system_generated'] = system_generated
|
||||
|
||||
new_user = models.User(**kwargs)
|
||||
|
||||
self._users[new_user.id] = new_user
|
||||
|
||||
if credentials is None:
|
||||
await self.async_save()
|
||||
return new_user
|
||||
|
||||
# Saving is done inside the link.
|
||||
await self.async_link_user(new_user, credentials)
|
||||
return new_user
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Add credentials to an existing user."""
|
||||
user.credentials.append(credentials)
|
||||
await self.async_save()
|
||||
credentials.is_new = False
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
self._users.pop(user.id)
|
||||
await self.async_save()
|
||||
|
||||
async def async_activate_user(self, user):
|
||||
"""Activate a user."""
|
||||
user.is_active = True
|
||||
await self.async_save()
|
||||
|
||||
async def async_deactivate_user(self, user):
|
||||
"""Activate a user."""
|
||||
user.is_active = False
|
||||
await self.async_save()
|
||||
|
||||
async def async_remove_credentials(self, credentials):
|
||||
"""Remove credentials."""
|
||||
for user in self._users.values():
|
||||
found = None
|
||||
|
||||
for index, cred in enumerate(user.credentials):
|
||||
if cred is credentials:
|
||||
found = index
|
||||
break
|
||||
|
||||
if found is not None:
|
||||
user.credentials.pop(found)
|
||||
break
|
||||
|
||||
await self.async_save()
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id=None):
|
||||
"""Create a new token for a user."""
|
||||
refresh_token = models.RefreshToken(user=user, client_id=client_id)
|
||||
user.refresh_tokens[refresh_token.token] = refresh_token
|
||||
await self.async_save()
|
||||
return refresh_token
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
|
||||
for user in self._users.values():
|
||||
refresh_token = user.refresh_tokens.get(token)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
|
||||
return None
|
||||
|
||||
async def async_load(self):
|
||||
"""Load the users."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
# Make sure that we're not overriding data if 2 loads happened at the
|
||||
# same time
|
||||
if self._users is not None:
|
||||
return
|
||||
|
||||
users = OrderedDict()
|
||||
|
||||
if data is None:
|
||||
self._users = users
|
||||
return
|
||||
|
||||
for user_dict in data['users']:
|
||||
users[user_dict['id']] = models.User(**user_dict)
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||
id=cred_dict['id'],
|
||||
is_new=False,
|
||||
auth_provider_type=cred_dict['auth_provider_type'],
|
||||
auth_provider_id=cred_dict['auth_provider_id'],
|
||||
data=cred_dict['data'],
|
||||
))
|
||||
|
||||
refresh_tokens = OrderedDict()
|
||||
|
||||
for rt_dict in data['refresh_tokens']:
|
||||
token = models.RefreshToken(
|
||||
id=rt_dict['id'],
|
||||
user=users[rt_dict['user_id']],
|
||||
client_id=rt_dict['client_id'],
|
||||
created_at=dt_util.parse_datetime(rt_dict['created_at']),
|
||||
access_token_expiration=timedelta(
|
||||
seconds=rt_dict['access_token_expiration']),
|
||||
token=rt_dict['token'],
|
||||
)
|
||||
refresh_tokens[token.id] = token
|
||||
users[rt_dict['user_id']].refresh_tokens[token.token] = token
|
||||
|
||||
for ac_dict in data['access_tokens']:
|
||||
refresh_token = refresh_tokens[ac_dict['refresh_token_id']]
|
||||
token = models.AccessToken(
|
||||
refresh_token=refresh_token,
|
||||
created_at=dt_util.parse_datetime(ac_dict['created_at']),
|
||||
token=ac_dict['token'],
|
||||
)
|
||||
refresh_token.access_tokens.append(token)
|
||||
|
||||
self._users = users
|
||||
|
||||
async def async_save(self):
|
||||
"""Save users."""
|
||||
users = [
|
||||
{
|
||||
'id': user.id,
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'name': user.name,
|
||||
'system_generated': user.system_generated,
|
||||
}
|
||||
for user in self._users.values()
|
||||
]
|
||||
|
||||
credentials = [
|
||||
{
|
||||
'id': credential.id,
|
||||
'user_id': user.id,
|
||||
'auth_provider_type': credential.auth_provider_type,
|
||||
'auth_provider_id': credential.auth_provider_id,
|
||||
'data': credential.data,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for credential in user.credentials
|
||||
]
|
||||
|
||||
refresh_tokens = [
|
||||
{
|
||||
'id': refresh_token.id,
|
||||
'user_id': user.id,
|
||||
'client_id': refresh_token.client_id,
|
||||
'created_at': refresh_token.created_at.isoformat(),
|
||||
'access_token_expiration':
|
||||
refresh_token.access_token_expiration.total_seconds(),
|
||||
'token': refresh_token.token,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
]
|
||||
|
||||
access_tokens = [
|
||||
{
|
||||
'id': user.id,
|
||||
'refresh_token_id': refresh_token.id,
|
||||
'created_at': access_token.created_at.isoformat(),
|
||||
'token': access_token.token,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
for access_token in refresh_token.access_tokens
|
||||
]
|
||||
|
||||
data = {
|
||||
'users': users,
|
||||
'credentials': credentials,
|
||||
'access_tokens': access_tokens,
|
||||
'refresh_tokens': refresh_tokens,
|
||||
}
|
||||
|
||||
await self._store.async_save(data, delay=1)
|
||||
4
homeassistant/auth/const.py
Normal file
4
homeassistant/auth/const.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Constants for the auth module."""
|
||||
from datetime import timedelta
|
||||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
75
homeassistant/auth/models.py
Normal file
75
homeassistant/auth/models.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Auth models."""
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ACCESS_TOKEN_EXPIRATION
|
||||
from .util import generate_secret
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class User:
|
||||
"""A user."""
|
||||
|
||||
name = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
system_generated = attr.ib(type=bool, default=False)
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False)
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class RefreshToken:
|
||||
"""RefreshToken for a user to grant new access tokens."""
|
||||
|
||||
user = attr.ib(type=User)
|
||||
client_id = attr.ib(type=str)
|
||||
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)))
|
||||
access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class AccessToken:
|
||||
"""Access token to access the API.
|
||||
|
||||
These will only ever be stored in memory and not be persisted.
|
||||
"""
|
||||
|
||||
refresh_token = attr.ib(type=RefreshToken)
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(generate_secret))
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
"""Return if this token has expired."""
|
||||
expires = self.created_at + self.refresh_token.access_token_expiration
|
||||
return dt_util.utcnow() > expires
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
# Allow the auth provider to store data to represent their auth.
|
||||
data = attr.ib(type=dict)
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_new = attr.ib(type=bool, default=True)
|
||||
139
homeassistant/auth/providers/__init__.py
Normal file
139
homeassistant/auth/providers/__init__.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from homeassistant.auth.models import Credentials
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_REQS = 'auth_prov_reqs_processed'
|
||||
|
||||
AUTH_PROVIDERS = Registry()
|
||||
|
||||
AUTH_PROVIDER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): str,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
# Specify ID if you have two auth providers for same type.
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def auth_provider_from_config(hass, store, config):
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config)
|
||||
|
||||
|
||||
async def load_auth_provider_module(hass, provider):
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
'homeassistant.auth.providers.{}'.format(provider))
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find auth provider %s', provider)
|
||||
return None
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
||||
if processed is None:
|
||||
processed = hass.data[DATA_REQS] = set()
|
||||
elif provider in processed:
|
||||
return module
|
||||
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
|
||||
processed.add(provider)
|
||||
return module
|
||||
|
||||
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||
|
||||
def __init__(self, hass, store, config):
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
"""
|
||||
return self.config.get(CONF_ID)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Return type of the provider."""
|
||||
return self.config[CONF_TYPE]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the auth provider."""
|
||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||
|
||||
async def async_credentials(self):
|
||||
"""Return all credentials of this provider."""
|
||||
users = await self.store.async_get_users()
|
||||
return [
|
||||
credentials
|
||||
for user in users
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == self.type and
|
||||
credentials.auth_provider_id == self.id)
|
||||
]
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data):
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type,
|
||||
auth_provider_id=self.id,
|
||||
data=data,
|
||||
)
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return the data flow for logging in with auth provider."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return {}
|
||||
@@ -6,15 +6,29 @@ import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import json
|
||||
|
||||
from homeassistant.auth.util import generate_secret
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_provider.homeassistant'
|
||||
|
||||
|
||||
PATH_DATA = '.users.json'
|
||||
def _disallow_id(conf):
|
||||
"""Disallow ID in config."""
|
||||
if CONF_ID in conf:
|
||||
raise vol.Invalid(
|
||||
'ID is not allowed for the homeassistant auth provider.')
|
||||
|
||||
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
return conf
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id)
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
@@ -31,14 +45,22 @@ class InvalidUser(HomeAssistantError):
|
||||
class Data:
|
||||
"""Hold the user data."""
|
||||
|
||||
def __init__(self, path, data):
|
||||
def __init__(self, hass):
|
||||
"""Initialize the user data store."""
|
||||
self.path = path
|
||||
self.hass = hass
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
self._data = None
|
||||
|
||||
async def async_load(self):
|
||||
"""Load stored data."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
if data is None:
|
||||
data = {
|
||||
'salt': auth.generate_secret(),
|
||||
'salt': generate_secret(),
|
||||
'users': []
|
||||
}
|
||||
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
@@ -77,8 +99,8 @@ class Data:
|
||||
hashed = base64.b64encode(hashed).decode()
|
||||
return hashed
|
||||
|
||||
def add_user(self, username, password):
|
||||
"""Add a user."""
|
||||
def add_auth(self, username, password):
|
||||
"""Add a new authenticated user/pass."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
raise InvalidUser
|
||||
|
||||
@@ -87,8 +109,22 @@ class Data:
|
||||
'password': self.hash_password(password, True),
|
||||
})
|
||||
|
||||
@callback
|
||||
def async_remove_auth(self, username):
|
||||
"""Remove authentication."""
|
||||
index = None
|
||||
for i, user in enumerate(self.users):
|
||||
if user['username'] == username:
|
||||
index = i
|
||||
break
|
||||
|
||||
if index is None:
|
||||
raise InvalidUser
|
||||
|
||||
self.users.pop(index)
|
||||
|
||||
def change_password(self, username, new_password):
|
||||
"""Update the password of a user.
|
||||
"""Update the password.
|
||||
|
||||
Raises InvalidUser if user cannot be found.
|
||||
"""
|
||||
@@ -99,34 +135,38 @@ class Data:
|
||||
else:
|
||||
raise InvalidUser
|
||||
|
||||
def save(self):
|
||||
async def async_save(self):
|
||||
"""Save data."""
|
||||
json.save_json(self.path, self._data)
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
|
||||
def load_data(path):
|
||||
"""Load auth data."""
|
||||
return Data(path, json.load_json(path, None))
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('homeassistant')
|
||||
class HassAuthProvider(auth.AuthProvider):
|
||||
@AUTH_PROVIDERS.register('homeassistant')
|
||||
class HassAuthProvider(AuthProvider):
|
||||
"""Auth provider based on a local storage of users in HASS config dir."""
|
||||
|
||||
DEFAULT_TITLE = 'Home Assistant Local'
|
||||
|
||||
data = None
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Initialize the auth provider."""
|
||||
if self.data is not None:
|
||||
return
|
||||
|
||||
self.data = Data(self.hass)
|
||||
await self.data.async_load()
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
|
||||
async def async_validate_login(self, username, password):
|
||||
"""Helper to validate a username and password."""
|
||||
def validate():
|
||||
"""Validate creds."""
|
||||
data = self._auth_data()
|
||||
data.validate_login(username, password)
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
|
||||
await self.hass.async_add_job(validate)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.validate_login, username, password)
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
@@ -141,9 +181,23 @@ class HassAuthProvider(auth.AuthProvider):
|
||||
'username': username
|
||||
})
|
||||
|
||||
def _auth_data(self):
|
||||
"""Return the auth provider data."""
|
||||
return load_data(self.hass.config.path(PATH_DATA))
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Get extra info for this credential."""
|
||||
return {
|
||||
'name': credentials.data['username']
|
||||
}
|
||||
|
||||
async def async_will_remove_credentials(self, credentials):
|
||||
"""When credentials get removed, also remove the auth."""
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
|
||||
try:
|
||||
self.data.async_remove_auth(credentials.data['username'])
|
||||
await self.data.async_save()
|
||||
except InvalidUser:
|
||||
# Can happen if somehow we didn't clean up a credential
|
||||
pass
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
@@ -5,9 +5,11 @@ import hmac
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
vol.Required('username'): str,
|
||||
@@ -16,7 +18,7 @@ USER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
vol.Required('users'): [USER_SCHEMA]
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
@@ -25,8 +27,8 @@ class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('insecure_example')
|
||||
class ExampleAuthProvider(auth.AuthProvider):
|
||||
@AUTH_PROVIDERS.register('insecure_example')
|
||||
class ExampleAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
async def async_credential_flow(self):
|
||||
107
homeassistant/auth/providers/legacy_api_password.py
Normal file
107
homeassistant/auth/providers/legacy_api_password.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Support Legacy API password auth provider.
|
||||
|
||||
It will be removed when auth system production ready
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
vol.Required('username'): str,
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
LEGACY_USER = 'homeassistant'
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('legacy_api_password')
|
||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
DEFAULT_TITLE = 'Legacy API Password'
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
|
||||
@callback
|
||||
def async_validate_login(self, password):
|
||||
"""Helper to validate a username and password."""
|
||||
if not hasattr(self.hass, 'http'):
|
||||
raise ValueError('http component is not loaded')
|
||||
|
||||
if self.hass.http.api_password is None:
|
||||
raise ValueError('http component is not configured using'
|
||||
' api_password')
|
||||
|
||||
if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Return LEGACY_USER always."""
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == LEGACY_USER:
|
||||
return credential
|
||||
|
||||
return self.async_create_credentials({
|
||||
'username': LEGACY_USER
|
||||
})
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""
|
||||
Set name as LEGACY_USER always.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return {'name': LEGACY_USER}
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
self._auth_provider.async_validate_login(
|
||||
user_input['password'])
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data={}
|
||||
)
|
||||
|
||||
schema = OrderedDict()
|
||||
schema['password'] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
)
|
||||
13
homeassistant/auth/util.py
Normal file
13
homeassistant/auth/util.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Auth utils."""
|
||||
import binascii
|
||||
import os
|
||||
|
||||
|
||||
def generate_secret(entropy: int = 32) -> str:
|
||||
"""Generate a secret.
|
||||
|
||||
Backport of secrets.token_hex from Python 3.6
|
||||
|
||||
Event loop friendly.
|
||||
"""
|
||||
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
|
||||
@@ -1 +0,0 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
@@ -28,9 +28,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
# hass.data key for logging information.
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
|
||||
'introduction', 'frontend', 'history'))
|
||||
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
|
||||
'logger', 'introduction', 'frontend', 'history'}
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
@@ -95,7 +94,8 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
await hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||
await hass.async_add_executor_job(
|
||||
conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
@@ -123,7 +123,6 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
components.update(hass.config_entries.async_domains())
|
||||
|
||||
# setup components
|
||||
# pylint: disable=not-an-iterable
|
||||
res = await core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
@@ -138,7 +137,7 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
for component in components:
|
||||
if component not in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
hass.async_create_task(async_setup_component(hass, component, config))
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -146,7 +145,7 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
for component in components:
|
||||
if component in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
hass.async_create_task(async_setup_component(hass, component, config))
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -163,7 +162,8 @@ def from_config_file(config_path: str,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False):
|
||||
log_no_color: bool = False)\
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
@@ -188,7 +188,8 @@ async def async_from_config_file(config_path: str,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False):
|
||||
log_no_color: bool = False)\
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
@@ -205,7 +206,7 @@ async def async_from_config_file(config_path: str,
|
||||
log_no_color)
|
||||
|
||||
try:
|
||||
config_dict = await hass.async_add_job(
|
||||
config_dict = await hass.async_add_executor_job(
|
||||
conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Error loading %s: %s", config_path, err)
|
||||
|
||||
@@ -121,7 +121,7 @@ def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
@@ -154,6 +154,16 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
"""An abstract class for alarm control devices."""
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Support for HomematicIP alarm control panel.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED)
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HMIP_OPEN = 'OPEN'
|
||||
HMIP_ZONE_AWAY = 'EXTERNAL'
|
||||
HMIP_ZONE_HOME = 'INTERNAL'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP alarm control devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP alarm control panel from a config entry."""
|
||||
from homematicip.aio.group import AsyncSecurityZoneGroup
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for group in home.groups:
|
||||
if isinstance(group, AsyncSecurityZoneGroup):
|
||||
devices.append(HomematicipSecurityZone(home, group))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||
"""Representation of an HomematicIP security zone group."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the security zone group."""
|
||||
device.modelType = 'Group-SecurityZone'
|
||||
device.windowState = ''
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._device.active:
|
||||
if (self._device.sabotage or self._device.motionDetected or
|
||||
self._device.windowState == HMIP_OPEN):
|
||||
return STATE_ALARM_TRIGGERED
|
||||
|
||||
if self._device.label == HMIP_ZONE_HOME:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
await self._home.set_security_zones_activation(False, False)
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
await self._home.set_security_zones_activation(True, False)
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
await self._home.set_security_zones_activation(True, True)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the alarm control device."""
|
||||
# The base class is loading the battery property, but device doesn't
|
||||
# have this property - base class needs clean-up.
|
||||
return None
|
||||
@@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
MqttAvailability)
|
||||
CONF_RETAIN, MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -54,6 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
@@ -66,9 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, code, availability_topic,
|
||||
payload_available, payload_not_available):
|
||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code,
|
||||
availability_topic, payload_available, payload_not_available):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
@@ -77,6 +78,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
@@ -134,7 +136,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos)
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos,
|
||||
self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
@@ -145,7 +148,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos)
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos,
|
||||
self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
@@ -156,7 +160,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_away, self._qos)
|
||||
self.hass, self._command_topic, self._payload_arm_away, self._qos,
|
||||
self._retain)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
|
||||
@@ -107,7 +107,6 @@ class _DisplayCategory(object):
|
||||
THERMOSTAT = "THERMOSTAT"
|
||||
|
||||
# Indicates the endpoint is a television.
|
||||
# pylint: disable=invalid-name
|
||||
TV = "TV"
|
||||
|
||||
|
||||
@@ -271,11 +270,14 @@ class _AlexaInterface(object):
|
||||
"""Return properties serialized for an API response."""
|
||||
for prop in self.properties_supported():
|
||||
prop_name = prop['name']
|
||||
yield {
|
||||
'name': prop_name,
|
||||
'namespace': self.name(),
|
||||
'value': self.get_property(prop_name),
|
||||
}
|
||||
# pylint: disable=assignment-from-no-return
|
||||
prop_value = self.get_property(prop_name)
|
||||
if prop_value is not None:
|
||||
yield {
|
||||
'name': prop_name,
|
||||
'namespace': self.name(),
|
||||
'value': prop_value,
|
||||
}
|
||||
|
||||
|
||||
class _AlexaPowerController(_AlexaInterface):
|
||||
@@ -439,14 +441,17 @@ class _AlexaThermostatController(_AlexaInterface):
|
||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp = None
|
||||
if name == 'targetSetpoint':
|
||||
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
|
||||
temp = self.entity.attributes.get(climate.ATTR_TEMPERATURE)
|
||||
elif name == 'lowerSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
|
||||
elif name == 'upperSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
|
||||
if temp is None:
|
||||
else:
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
if temp is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
'value': float(temp),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
@@ -1474,9 +1479,6 @@ async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||
mode = mode if isinstance(mode, str) else mode['value']
|
||||
|
||||
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||
# Work around a pylint false positive due to
|
||||
# https://github.com/PyCQA/pylint/issues/1830
|
||||
# pylint: disable=stop-iteration-return
|
||||
ha_mode = next(
|
||||
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||
None
|
||||
|
||||
@@ -81,7 +81,6 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
# pylint: disable=no-self-use
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.1.7']
|
||||
REQUIREMENTS = ['pyarlo==0.1.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ a limited expiration.
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
@@ -112,13 +113,22 @@ from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import indieauth
|
||||
|
||||
from .client import verify_client
|
||||
|
||||
DOMAIN = 'auth'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
WS_TYPE_CURRENT_USER = 'auth/current_user'
|
||||
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CURRENT_USER,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -133,6 +143,11 @@ async def async_setup(hass, config):
|
||||
hass.http.register_view(GrantTokenView(retrieve_credentials))
|
||||
hass.http.register_view(LinkUserView(retrieve_credentials))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CURRENT_USER, websocket_current_user,
|
||||
SCHEMA_WS_CURRENT_USER
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -143,14 +158,13 @@ class AuthProvidersView(HomeAssistantView):
|
||||
name = 'api:auth:providers'
|
||||
requires_auth = False
|
||||
|
||||
@verify_client
|
||||
async def get(self, request, client):
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in request.app['hass'].auth.async_auth_providers])
|
||||
} for provider in request.app['hass'].auth.auth_providers])
|
||||
|
||||
|
||||
class LoginFlowIndexView(FlowManagerIndexView):
|
||||
@@ -164,16 +178,16 @@ class LoginFlowIndexView(FlowManagerIndexView):
|
||||
"""Do not allow index of flows in progress."""
|
||||
return aiohttp.web.Response(status=405)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('client_id'): str,
|
||||
vol.Required('handler'): vol.Any(str, list),
|
||||
vol.Required('redirect_uri'): str,
|
||||
}))
|
||||
async def post(self, request, client, data):
|
||||
async def post(self, request, data):
|
||||
"""Create a new login flow."""
|
||||
if data['redirect_uri'] not in client.redirect_uris:
|
||||
return self.json_message('invalid redirect uri', )
|
||||
if not indieauth.verify_redirect_uri(data['client_id'],
|
||||
data['redirect_uri']):
|
||||
return self.json_message('invalid client id or redirect uri', 400)
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
@@ -191,16 +205,20 @@ class LoginFlowResourceView(FlowManagerResourceView):
|
||||
super().__init__(flow_mgr)
|
||||
self._store_credentials = store_credentials
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
async def get(self, request):
|
||||
async def get(self, request, flow_id):
|
||||
"""Do not allow getting status of a flow in progress."""
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
|
||||
async def post(self, request, client, flow_id, data):
|
||||
@RequestDataValidator(vol.Schema({
|
||||
'client_id': str
|
||||
}, extra=vol.ALLOW_EXTRA))
|
||||
async def post(self, request, flow_id, data):
|
||||
"""Handle progressing a login flow request."""
|
||||
client_id = data.pop('client_id')
|
||||
|
||||
if not indieauth.verify_client_id(client_id):
|
||||
return self.json_message('Invalid client id', 400)
|
||||
|
||||
try:
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
@@ -212,7 +230,7 @@ class LoginFlowResourceView(FlowManagerResourceView):
|
||||
return self.json(self._prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_credentials(client.id, result['result'])
|
||||
result['result'] = self._store_credentials(client_id, result['result'])
|
||||
|
||||
return self.json(result)
|
||||
|
||||
@@ -228,20 +246,26 @@ class GrantTokenView(HomeAssistantView):
|
||||
"""Initialize the grant token view."""
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
|
||||
@verify_client
|
||||
async def post(self, request, client):
|
||||
async def post(self, request):
|
||||
"""Grant a token."""
|
||||
hass = request.app['hass']
|
||||
data = await request.post()
|
||||
|
||||
client_id = data.get('client_id')
|
||||
if client_id is None or not indieauth.verify_client_id(client_id):
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid client id',
|
||||
}, status_code=400)
|
||||
|
||||
grant_type = data.get('grant_type')
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(
|
||||
hass, client.id, data)
|
||||
return await self._async_handle_auth_code(hass, client_id, data)
|
||||
|
||||
elif grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(
|
||||
hass, client.id, data)
|
||||
hass, client_id, data)
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
@@ -261,9 +285,17 @@ class GrantTokenView(HomeAssistantView):
|
||||
if credentials is None:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid code',
|
||||
}, status_code=400)
|
||||
|
||||
user = await hass.auth.async_get_or_create_user(credentials)
|
||||
|
||||
if not user.is_active:
|
||||
return self.json({
|
||||
'error': 'access_denied',
|
||||
'error_description': 'User is not active',
|
||||
}, status_code=403)
|
||||
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user,
|
||||
client_id)
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
@@ -340,12 +372,43 @@ def _create_cred_store():
|
||||
def store_credentials(client_id, credentials):
|
||||
"""Store credentials and return a code to retrieve it."""
|
||||
code = uuid.uuid4().hex
|
||||
temp_credentials[(client_id, code)] = credentials
|
||||
temp_credentials[(client_id, code)] = (dt_util.utcnow(), credentials)
|
||||
return code
|
||||
|
||||
@callback
|
||||
def retrieve_credentials(client_id, code):
|
||||
"""Retrieve credentials."""
|
||||
return temp_credentials.pop((client_id, code), None)
|
||||
key = (client_id, code)
|
||||
|
||||
if key not in temp_credentials:
|
||||
return None
|
||||
|
||||
created, credentials = temp_credentials.pop(key)
|
||||
|
||||
# OAuth 4.2.1
|
||||
# The authorization code MUST expire shortly after it is issued to
|
||||
# mitigate the risk of leaks. A maximum authorization code lifetime of
|
||||
# 10 minutes is RECOMMENDED.
|
||||
if dt_util.utcnow() - created < timedelta(minutes=10):
|
||||
return credentials
|
||||
|
||||
return None
|
||||
|
||||
return store_credentials, retrieve_credentials
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_current_user(hass, connection, msg):
|
||||
"""Return the current user."""
|
||||
user = connection.request.get('hass_user')
|
||||
|
||||
if user is None:
|
||||
connection.to_write.put_nowait(websocket_api.error_message(
|
||||
msg['id'], 'no_user', 'Not authenticated as a user'))
|
||||
return
|
||||
|
||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'is_owner': user.is_owner,
|
||||
}))
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""Helpers to resolve client ID/secret."""
|
||||
import base64
|
||||
from functools import wraps
|
||||
import hmac
|
||||
|
||||
import aiohttp.hdrs
|
||||
|
||||
|
||||
def verify_client(method):
|
||||
"""Decorator to verify client id/secret on requests."""
|
||||
@wraps(method)
|
||||
async def wrapper(view, request, *args, **kwargs):
|
||||
"""Verify client id/secret before doing request."""
|
||||
client = await _verify_client(request)
|
||||
|
||||
if client is None:
|
||||
return view.json({
|
||||
'error': 'invalid_client',
|
||||
}, status_code=401)
|
||||
|
||||
return await method(
|
||||
view, request, *args, **kwargs, client=client)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def _verify_client(request):
|
||||
"""Method to verify the client id/secret in consistent time.
|
||||
|
||||
By using a consistent time for looking up client id and comparing the
|
||||
secret, we prevent attacks by malicious actors trying different client ids
|
||||
and are able to derive from the time it takes to process the request if
|
||||
they guessed the client id correctly.
|
||||
"""
|
||||
if aiohttp.hdrs.AUTHORIZATION not in request.headers:
|
||||
return None
|
||||
|
||||
auth_type, auth_value = \
|
||||
request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1)
|
||||
|
||||
if auth_type != 'Basic':
|
||||
return None
|
||||
|
||||
decoded = base64.b64decode(auth_value).decode('utf-8')
|
||||
try:
|
||||
client_id, client_secret = decoded.split(':', 1)
|
||||
except ValueError:
|
||||
# If no ':' in decoded
|
||||
client_id, client_secret = decoded, None
|
||||
|
||||
return await async_secure_get_client(
|
||||
request.app['hass'], client_id, client_secret)
|
||||
|
||||
|
||||
async def async_secure_get_client(hass, client_id, client_secret):
|
||||
"""Get a client id/secret in consistent time."""
|
||||
client = await hass.auth.async_get_client(client_id)
|
||||
|
||||
if client is None:
|
||||
if client_secret is not None:
|
||||
# Still do a compare so we run same time as if a client was found.
|
||||
hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client_secret.encode('utf-8'))
|
||||
return None
|
||||
|
||||
if client.secret is None:
|
||||
return client
|
||||
|
||||
elif client_secret is None:
|
||||
# Still do a compare so we run same time as if a secret was passed.
|
||||
hmac.compare_digest(client.secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8'))
|
||||
return None
|
||||
|
||||
elif hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8')):
|
||||
return client
|
||||
|
||||
return None
|
||||
130
homeassistant/components/auth/indieauth.py
Normal file
130
homeassistant/components/auth/indieauth.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Helpers to resolve client ID/secret."""
|
||||
from ipaddress import ip_address, ip_network
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# IP addresses of loopback interfaces
|
||||
ALLOWED_IPS = (
|
||||
ip_address('127.0.0.1'),
|
||||
ip_address('::1'),
|
||||
)
|
||||
|
||||
# RFC1918 - Address allocation for Private Internets
|
||||
ALLOWED_NETWORKS = (
|
||||
ip_network('10.0.0.0/8'),
|
||||
ip_network('172.16.0.0/12'),
|
||||
ip_network('192.168.0.0/16'),
|
||||
)
|
||||
|
||||
|
||||
def verify_redirect_uri(client_id, redirect_uri):
|
||||
"""Verify that the client and redirect uri match."""
|
||||
try:
|
||||
client_id_parts = _parse_client_id(client_id)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
redirect_parts = _parse_url(redirect_uri)
|
||||
|
||||
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
|
||||
# but needs to be specified in link tag when fetching `client_id`.
|
||||
# This is not implemented.
|
||||
|
||||
# Verify redirect url and client url have same scheme and domain.
|
||||
return (
|
||||
client_id_parts.scheme == redirect_parts.scheme and
|
||||
client_id_parts.netloc == redirect_parts.netloc
|
||||
)
|
||||
|
||||
|
||||
def verify_client_id(client_id):
|
||||
"""Verify that the client id is valid."""
|
||||
try:
|
||||
_parse_client_id(client_id)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _parse_url(url):
|
||||
"""Parse a url in parts and canonicalize according to IndieAuth."""
|
||||
parts = urlparse(url)
|
||||
|
||||
# Canonicalize a url according to IndieAuth 3.2.
|
||||
|
||||
# SHOULD convert the hostname to lowercase
|
||||
parts = parts._replace(netloc=parts.netloc.lower())
|
||||
|
||||
# If a URL with no path component is ever encountered,
|
||||
# it MUST be treated as if it had the path /.
|
||||
if parts.path == '':
|
||||
parts = parts._replace(path='/')
|
||||
|
||||
return parts
|
||||
|
||||
|
||||
def _parse_client_id(client_id):
|
||||
"""Test if client id is a valid URL according to IndieAuth section 3.2.
|
||||
|
||||
https://indieauth.spec.indieweb.org/#client-identifier
|
||||
"""
|
||||
parts = _parse_url(client_id)
|
||||
|
||||
# Client identifier URLs
|
||||
# MUST have either an https or http scheme
|
||||
if parts.scheme not in ('http', 'https'):
|
||||
raise ValueError()
|
||||
|
||||
# MUST contain a path component
|
||||
# Handled by url canonicalization.
|
||||
|
||||
# MUST NOT contain single-dot or double-dot path segments
|
||||
if any(segment in ('.', '..') for segment in parts.path.split('/')):
|
||||
raise ValueError(
|
||||
'Client ID cannot contain single-dot or double-dot path segments')
|
||||
|
||||
# MUST NOT contain a fragment component
|
||||
if parts.fragment != '':
|
||||
raise ValueError('Client ID cannot contain a fragment')
|
||||
|
||||
# MUST NOT contain a username or password component
|
||||
if parts.username is not None:
|
||||
raise ValueError('Client ID cannot contain username')
|
||||
|
||||
if parts.password is not None:
|
||||
raise ValueError('Client ID cannot contain password')
|
||||
|
||||
# MAY contain a port
|
||||
try:
|
||||
# parts raises ValueError when port cannot be parsed as int
|
||||
parts.port
|
||||
except ValueError:
|
||||
raise ValueError('Client ID contains invalid port')
|
||||
|
||||
# Additionally, hostnames
|
||||
# MUST be domain names or a loopback interface and
|
||||
# MUST NOT be IPv4 or IPv6 addresses except for IPv4 127.0.0.1
|
||||
# or IPv6 [::1]
|
||||
|
||||
# We are not goint to follow the spec here. We are going to allow
|
||||
# any internal network IP to be used inside a client id.
|
||||
|
||||
address = None
|
||||
|
||||
try:
|
||||
netloc = parts.netloc
|
||||
|
||||
# Strip the [, ] from ipv6 addresses before parsing
|
||||
if netloc[0] == '[' and netloc[-1] == ']':
|
||||
netloc = netloc[1:-1]
|
||||
|
||||
address = ip_address(netloc)
|
||||
except ValueError:
|
||||
# Not an ip address
|
||||
pass
|
||||
|
||||
if (address is None or
|
||||
address in ALLOWED_IPS or
|
||||
any(address in network for network in ALLOWED_NETWORKS)):
|
||||
return parts
|
||||
|
||||
raise ValueError('Hostname should be a domain name or local IP address')
|
||||
@@ -16,7 +16,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'bbb_gpio'
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
def setup(hass, config):
|
||||
"""Set up the BeagleBone Black GPIO component."""
|
||||
# pylint: disable=import-error
|
||||
@@ -34,41 +33,39 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
# noqa: F821
|
||||
|
||||
def setup_output(pin):
|
||||
"""Set up a GPIO as output."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
|
||||
|
||||
def setup_input(pin, pull_mode):
|
||||
"""Set up a GPIO as input."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.setup(pin, GPIO.IN, # noqa: F821
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821
|
||||
else GPIO.PUD_UP) # noqa: F821
|
||||
GPIO.setup(pin, GPIO.IN,
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN'
|
||||
else GPIO.PUD_UP)
|
||||
|
||||
|
||||
def write_output(pin, value):
|
||||
"""Write a value to a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.output(pin, value)
|
||||
|
||||
|
||||
def read_input(pin):
|
||||
"""Read a value from a GPIO."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
return GPIO.input(pin) is GPIO.HIGH
|
||||
|
||||
|
||||
def edge_detect(pin, event_callback, bounce):
|
||||
"""Add detection for RISING and FALLING events."""
|
||||
# pylint: disable=import-error,undefined-variable
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
GPIO.add_event_detect(
|
||||
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||
|
||||
@@ -67,7 +67,6 @@ async def async_unload_entry(hass, entry):
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class BinarySensorDevice(Entity):
|
||||
"""Represent a binary sensor."""
|
||||
|
||||
|
||||
@@ -124,11 +124,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
result['check_control_messages'] = check_control_messages
|
||||
elif self._attribute == 'charging_status':
|
||||
result['charging_status'] = vehicle_state.charging_status.value
|
||||
# pylint: disable=W0212
|
||||
# pylint: disable=protected-access
|
||||
result['last_charging_end_result'] = \
|
||||
vehicle_state._attributes['lastChargingEndResult']
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=W0212
|
||||
# pylint: disable=protected-access
|
||||
result['connection_status'] = \
|
||||
vehicle_state._attributes['connectionStatus']
|
||||
|
||||
@@ -166,7 +166,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
# device class plug: On means device is plugged in,
|
||||
# Off means device is unplugged
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=W0212
|
||||
# pylint: disable=protected-access
|
||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
||||
'CONNECTED')
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import (
|
||||
CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID,
|
||||
DATA_DECONZ_UNSUB)
|
||||
from homeassistant.components.deconz.const import (
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -62,7 +62,8 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""
|
||||
if reason['state'] or \
|
||||
'reachable' in reason['attr'] or \
|
||||
'battery' in reason['attr']:
|
||||
'battery' in reason['attr'] or \
|
||||
'on' in reason['attr']:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@@ -107,6 +108,8 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
attr = {}
|
||||
if self._sensor.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||
if self._sensor.on is not None:
|
||||
attr[ATTR_ON] = self._sensor.on
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
|
||||
attr['dark'] = self._sensor.dark
|
||||
attr[ATTR_DARK] = self._sensor.dark
|
||||
return attr
|
||||
|
||||
@@ -14,7 +14,8 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.components.digital_ocean import (
|
||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
|
||||
ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -75,6 +76,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the Digital Ocean droplet."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
ATTR_CREATED_AT: self.data.created_at,
|
||||
ATTR_DROPLET_ID: self.data.id,
|
||||
ATTR_DROPLET_NAME: self.data.name,
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
|
||||
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
|
||||
REQUIREMENTS = ['pyflic-homeassistant==0.4.dev0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ class GC100BinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, name, port_addr, gc100):
|
||||
"""Initialize the GC100 binary sensor."""
|
||||
# pylint: disable=no-member
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port_addr = port_addr
|
||||
self._gc100 = gc100
|
||||
|
||||
@@ -9,8 +9,8 @@ import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
@@ -21,17 +21,18 @@ ATTR_EVENT_DELAY = 'event_delay'
|
||||
ATTR_MOTION_DETECTED = 'motion_detected'
|
||||
ATTR_ILLUMINATION = 'illumination'
|
||||
|
||||
HMIP_OPEN = 'open'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP binary sensor devices."""
|
||||
"""Set up the binary sensor devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP binary sensor from a config entry."""
|
||||
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, ShutterContact):
|
||||
@@ -58,11 +59,13 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the shutter contact is on/open."""
|
||||
from homematicip.base.enums import WindowState
|
||||
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
if self._device.windowState is None:
|
||||
return None
|
||||
return self._device.windowState.lower() == HMIP_OPEN
|
||||
return self._device.windowState == WindowState.OPEN
|
||||
|
||||
|
||||
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
|
||||
@@ -8,7 +8,7 @@ https://home-assistant.io/components/binary_sensor.isy994/
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Callable # noqa
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
|
||||
|
||||
@@ -52,19 +52,18 @@ class LinodeBinarySensor(BinarySensorDevice):
|
||||
self._node_id = node_id
|
||||
self._state = None
|
||||
self.data = None
|
||||
self._attrs = {}
|
||||
self._name = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
if self.data is not None:
|
||||
return self.data.label
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.data is not None:
|
||||
return self.data.status == 'running'
|
||||
return False
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@@ -74,8 +73,18 @@ class LinodeBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the Linode Node."""
|
||||
if self.data:
|
||||
return {
|
||||
return self._attrs
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
self._linode.update()
|
||||
if self._linode.data is not None:
|
||||
for node in self._linode.data:
|
||||
if node.id == self._node_id:
|
||||
self.data = node
|
||||
if self.data is not None:
|
||||
self._state = self.data.status == 'running'
|
||||
self._attrs = {
|
||||
ATTR_CREATED: self.data.created,
|
||||
ATTR_NODE_ID: self.data.id,
|
||||
ATTR_NODE_NAME: self.data.label,
|
||||
@@ -85,12 +94,4 @@ class LinodeBinarySensor(BinarySensorDevice):
|
||||
ATTR_REGION: self.data.region.country,
|
||||
ATTR_VCPUS: self.data.specs.vcpus,
|
||||
}
|
||||
return {}
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
self._linode.update()
|
||||
if self._linode.data is not None:
|
||||
for node in self._linode.data:
|
||||
if node.id == self._node_id:
|
||||
self.data = node
|
||||
self._name = self.data.label
|
||||
|
||||
@@ -29,7 +29,8 @@ async def async_setup_platform(
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
||||
class MySensorsBinarySensor(
|
||||
mysensors.device.MySensorsEntity, BinarySensorDevice):
|
||||
"""Representation of a MySensors Binary Sensor child node."""
|
||||
|
||||
@property
|
||||
|
||||
@@ -31,12 +31,10 @@ CAMERA_BINARY_TYPES = {
|
||||
|
||||
STRUCTURE_BINARY_TYPES = {
|
||||
'away': None,
|
||||
# 'security_state', # pending python-nest update
|
||||
}
|
||||
|
||||
STRUCTURE_BINARY_STATE_MAP = {
|
||||
'away': {'away': True, 'home': False},
|
||||
'security_state': {'deter': True, 'ok': False},
|
||||
}
|
||||
|
||||
_BINARY_TYPES_DEPRECATED = [
|
||||
@@ -135,7 +133,7 @@ class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
|
||||
value = getattr(self.device, self.variable)
|
||||
if self.variable in STRUCTURE_BINARY_TYPES:
|
||||
self._state = bool(STRUCTURE_BINARY_STATE_MAP
|
||||
[self.variable][value])
|
||||
[self.variable].get(value))
|
||||
else:
|
||||
self._state = bool(value)
|
||||
|
||||
|
||||
127
homeassistant/components/binary_sensor/rachio.py
Normal file
127
homeassistant/components/binary_sensor/rachio.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Integration with the Rachio Iro sprinkler system controller.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rachio/
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO,
|
||||
KEY_DEVICE_ID,
|
||||
KEY_STATUS,
|
||||
KEY_SUBTYPE,
|
||||
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||
STATUS_OFFLINE,
|
||||
STATUS_ONLINE,
|
||||
SUBTYPE_OFFLINE,
|
||||
SUBTYPE_ONLINE,)
|
||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['rachio']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Rachio binary sensors."""
|
||||
devices = []
|
||||
for controller in hass.data[DOMAIN_RACHIO].controllers:
|
||||
devices.append(RachioControllerOnlineBinarySensor(hass, controller))
|
||||
|
||||
add_devices(devices)
|
||||
_LOGGER.info("%d Rachio binary sensor(s) added", len(devices))
|
||||
|
||||
|
||||
class RachioControllerBinarySensor(BinarySensorDevice):
|
||||
"""Represent a binary sensor that reflects a Rachio state."""
|
||||
|
||||
def __init__(self, hass, controller, poll=True):
|
||||
"""Set up a new Rachio controller binary sensor."""
|
||||
self._controller = controller
|
||||
|
||||
if poll:
|
||||
self._state = self._poll_update()
|
||||
else:
|
||||
self._state = None
|
||||
|
||||
dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||
self._handle_any_update)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Declare that this entity pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the sensor has a 'true' value."""
|
||||
return self._state
|
||||
|
||||
def _handle_any_update(self, *args, **kwargs) -> None:
|
||||
"""Determine whether an update event applies to this device."""
|
||||
if args[0][KEY_DEVICE_ID] != self._controller.controller_id:
|
||||
# For another device
|
||||
return
|
||||
|
||||
# For this device
|
||||
self._handle_update()
|
||||
|
||||
@abstractmethod
|
||||
def _poll_update(self, data=None) -> bool:
|
||||
"""Request the state from the API."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle an update to the state of this sensor."""
|
||||
pass
|
||||
|
||||
|
||||
class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
|
||||
"""Represent a binary sensor that reflects if the controller is online."""
|
||||
|
||||
def __init__(self, hass, controller):
|
||||
"""Set up a new Rachio controller online binary sensor."""
|
||||
super().__init__(hass, controller, poll=False)
|
||||
self._state = self._poll_update(controller.init_data)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of this sensor including the controller name."""
|
||||
return "{} online".format(self._controller.name)
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'connectivity'
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the name of an icon for this sensor."""
|
||||
return 'mdi:wifi-strength-4' if self.is_on\
|
||||
else 'mdi:wifi-strength-off-outline'
|
||||
|
||||
def _poll_update(self, data=None) -> bool:
|
||||
"""Request the state from the API."""
|
||||
if data is None:
|
||||
data = self._controller.rachio.device.get(
|
||||
self._controller.controller_id)[1]
|
||||
|
||||
if data[KEY_STATUS] == STATUS_ONLINE:
|
||||
return True
|
||||
elif data[KEY_STATUS] == STATUS_OFFLINE:
|
||||
return False
|
||||
else:
|
||||
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
|
||||
data[KEY_STATUS])
|
||||
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle an update to the state of this sensor."""
|
||||
if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE:
|
||||
self._state = True
|
||||
elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
|
||||
self._state = False
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
@@ -23,7 +23,7 @@ DEPENDENCIES = ['ring']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
# Sensor types: Name, category, device_class
|
||||
SENSOR_TYPES = {
|
||||
|
||||
@@ -58,7 +58,6 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
|
||||
"""Initialize the RPi binary sensor."""
|
||||
# pylint: disable=no-member
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port = port
|
||||
self._pull_mode = pull_mode
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.14.3']
|
||||
REQUIREMENTS = ['numpy==1.14.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ DEPENDENCIES = ['wemo']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=too-many-function-args
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Register discovered WeMo binary sensors."""
|
||||
import pywemo.discovery as discovery
|
||||
|
||||
@@ -41,8 +41,9 @@ async def async_setup(hass, config):
|
||||
hass.http.register_view(CalendarListView(component))
|
||||
hass.http.register_view(CalendarEventView(component))
|
||||
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
'calendar', 'calendar', 'hass:calendar')
|
||||
# Doesn't work in prod builds of the frontend: home-assistant-polymer#1289
|
||||
# await hass.components.frontend.async_register_built_in_panel(
|
||||
# 'calendar', 'calendar', 'hass:calendar')
|
||||
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Google Calendar Search binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.google_calendar/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -89,9 +88,7 @@ class GoogleCalendarData(object):
|
||||
params['timeMin'] = start_date.isoformat('T')
|
||||
params['timeMax'] = end_date.isoformat('T')
|
||||
|
||||
# pylint: disable=no-member
|
||||
events = await hass.async_add_job(service.events)
|
||||
# pylint: enable=no-member
|
||||
result = await hass.async_add_job(events.list(**params).execute)
|
||||
|
||||
items = result.get('items', [])
|
||||
@@ -111,7 +108,7 @@ class GoogleCalendarData(object):
|
||||
service, params = self._prepare_query()
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
|
||||
events = service.events() # pylint: disable=no-member
|
||||
events = service.events()
|
||||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
|
||||
@@ -66,8 +66,8 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
|
||||
|
||||
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'
|
||||
SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
'type': WS_TYPE_CAMERA_THUMBNAIL,
|
||||
'entity_id': cv.entity_id
|
||||
vol.Required('type'): WS_TYPE_CAMERA_THUMBNAIL,
|
||||
vol.Required('entity_id'): cv.entity_id
|
||||
})
|
||||
|
||||
|
||||
@@ -322,6 +322,7 @@ class Camera(Entity):
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
response = None
|
||||
raise
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
|
||||
@@ -10,12 +10,13 @@ from datetime import timedelta
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.neato import (
|
||||
NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['neato']
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Neato Camera."""
|
||||
@@ -45,7 +46,6 @@ class NeatoCleaningMap(Camera):
|
||||
self.update()
|
||||
return self._image
|
||||
|
||||
@Throttle(timedelta(seconds=10))
|
||||
def update(self):
|
||||
"""Check the contents of the map list."""
|
||||
self.neato.update_robots()
|
||||
|
||||
@@ -25,9 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['onvif-py3==0.1.3',
|
||||
'suds-py3==1.3.3.0',
|
||||
'http://github.com/tgaugry/suds-passworddigest-py3'
|
||||
'/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip'
|
||||
'#suds-passworddigest-py3==0.1.2a']
|
||||
'suds-passworddigest-homeassistant==0.1.2a0.dev0']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
DEFAULT_NAME = 'ONVIF Camera'
|
||||
DEFAULT_PORT = 5000
|
||||
|
||||
@@ -233,6 +233,7 @@ class ProxyCamera(Camera):
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
req.close()
|
||||
response = None
|
||||
raise
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
|
||||
170
homeassistant/components/camera/push.py
Normal file
170
homeassistant/components/camera/push.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Camera platform that receives images through HTTP POST.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/camera.push/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from collections import deque
|
||||
from datetime import timedelta
|
||||
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.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BUFFER_SIZE = 'buffer'
|
||||
CONF_IMAGE_FIELD = 'field'
|
||||
|
||||
DEFAULT_NAME = "Push Camera"
|
||||
|
||||
ATTR_FILENAME = 'filename'
|
||||
ATTR_LAST_TRIP = 'last_trip'
|
||||
|
||||
PUSH_CAMERA_DATA = 'push_camera'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int,
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Push Camera platform."""
|
||||
if PUSH_CAMERA_DATA not in hass.data:
|
||||
hass.data[PUSH_CAMERA_DATA] = {}
|
||||
|
||||
cameras = [PushCamera(config[CONF_NAME],
|
||||
config[CONF_BUFFER_SIZE],
|
||||
config[CONF_TIMEOUT])]
|
||||
|
||||
hass.http.register_view(CameraPushReceiver(hass,
|
||||
config[CONF_IMAGE_FIELD]))
|
||||
|
||||
async_add_devices(cameras)
|
||||
|
||||
|
||||
class CameraPushReceiver(HomeAssistantView):
|
||||
"""Handle pushes from remote camera."""
|
||||
|
||||
url = "/api/camera_push/{entity_id}"
|
||||
name = 'api:camera_push:camera_entity'
|
||||
|
||||
def __init__(self, hass, image_field):
|
||||
"""Initialize CameraPushReceiver with camera entity."""
|
||||
self._cameras = hass.data[PUSH_CAMERA_DATA]
|
||||
self._image = image_field
|
||||
|
||||
async def post(self, request, entity_id):
|
||||
"""Accept the POST from Camera."""
|
||||
_camera = self._cameras.get(entity_id)
|
||||
|
||||
if _camera is None:
|
||||
_LOGGER.error("Unknown %s", entity_id)
|
||||
return self.json_message('Unknown {}'.format(entity_id),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
data = await request.post()
|
||||
_LOGGER.debug("Received Camera push: %s", data[self._image])
|
||||
await _camera.update_image(data[self._image].file.read(),
|
||||
data[self._image].filename)
|
||||
except ValueError as value_error:
|
||||
_LOGGER.error("Unknown value %s", value_error)
|
||||
return self.json_message('Invalid POST', HTTP_BAD_REQUEST)
|
||||
except KeyError as key_error:
|
||||
_LOGGER.error('In your POST message %s', key_error)
|
||||
return self.json_message('{} missing'.format(self._image),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
class PushCamera(Camera):
|
||||
"""The representation of a Push camera."""
|
||||
|
||||
def __init__(self, name, buffer_size, timeout):
|
||||
"""Initialize push camera component."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._last_trip = None
|
||||
self._filename = None
|
||||
self._expired_listener = None
|
||||
self._state = STATE_IDLE
|
||||
self._timeout = timeout
|
||||
self.queue = deque([], buffer_size)
|
||||
self._current_image = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.data[PUSH_CAMERA_DATA][self.entity_id] = self
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Current state of the camera."""
|
||||
return self._state
|
||||
|
||||
async def update_image(self, image, filename):
|
||||
"""Update the camera image."""
|
||||
if self._state == STATE_IDLE:
|
||||
self._state = STATE_RECORDING
|
||||
self._last_trip = dt_util.utcnow()
|
||||
self.queue.clear()
|
||||
|
||||
self._filename = filename
|
||||
self.queue.appendleft(image)
|
||||
|
||||
@callback
|
||||
def reset_state(now):
|
||||
"""Set state to idle after no new images for a period of time."""
|
||||
self._state = STATE_IDLE
|
||||
self._expired_listener = None
|
||||
_LOGGER.debug("Reset state")
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._expired_listener:
|
||||
self._expired_listener()
|
||||
|
||||
self._expired_listener = async_track_point_in_utc_time(
|
||||
self.hass, reset_state, dt_util.utcnow() + self._timeout)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response."""
|
||||
if self.queue:
|
||||
if self._state == STATE_IDLE:
|
||||
self.queue.rotate(1)
|
||||
self._current_image = self.queue[0]
|
||||
|
||||
return self._current_image
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
name: value for name, value in (
|
||||
(ATTR_LAST_TRIP, self._last_trip),
|
||||
(ATTR_FILENAME, self._filename),
|
||||
) if value is not None
|
||||
}
|
||||
@@ -67,8 +67,6 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
]
|
||||
|
||||
for cam in config.get(CONF_CAMERAS, []):
|
||||
# https://github.com/PyCQA/pylint/issues/1830
|
||||
# pylint: disable=stop-iteration-return
|
||||
camera = next(
|
||||
(dc for dc in discovered_cameras
|
||||
if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None)
|
||||
|
||||
@@ -104,27 +104,25 @@ class XiaomiCamera(Camera):
|
||||
|
||||
dirs = [d for d in ftp.nlst() if '.' not in d]
|
||||
if not dirs:
|
||||
if self._model == MODEL_YI:
|
||||
_LOGGER.warning("There don't appear to be any uploaded videos")
|
||||
return False
|
||||
elif self._model == MODEL_XIAOFANG:
|
||||
_LOGGER.warning("There don't appear to be any folders")
|
||||
return False
|
||||
_LOGGER.warning("There don't appear to be any folders")
|
||||
return False
|
||||
|
||||
first_dir = dirs[-1]
|
||||
try:
|
||||
ftp.cwd(first_dir)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Unable to find path: %s - %s', first_dir, exc)
|
||||
return False
|
||||
first_dir = dirs[-1]
|
||||
try:
|
||||
ftp.cwd(first_dir)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Unable to find path: %s - %s', first_dir, exc)
|
||||
return False
|
||||
|
||||
if self._model == MODEL_XIAOFANG:
|
||||
dirs = [d for d in ftp.nlst() if '.' not in d]
|
||||
if not dirs:
|
||||
_LOGGER.warning("There don't appear to be any uploaded videos")
|
||||
return False
|
||||
|
||||
latest_dir = dirs[-1]
|
||||
ftp.cwd(latest_dir)
|
||||
latest_dir = dirs[-1]
|
||||
ftp.cwd(latest_dir)
|
||||
|
||||
videos = [v for v in ftp.nlst() if '.tmp' not in v]
|
||||
if not videos:
|
||||
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
|
||||
|
||||
@@ -53,7 +53,6 @@ class YiCamera(Camera):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._ftp = None
|
||||
self._last_image = None
|
||||
self._last_url = None
|
||||
self._manager = hass.data[DATA_FFMPEG]
|
||||
@@ -64,8 +63,6 @@ class YiCamera(Camera):
|
||||
self.user = config[CONF_USERNAME]
|
||||
self.passwd = config[CONF_PASSWORD]
|
||||
|
||||
hass.async_add_job(self._connect_to_client)
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
@@ -76,38 +73,35 @@ class YiCamera(Camera):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
async def _connect_to_client(self):
|
||||
"""Attempt to establish a connection via FTP."""
|
||||
async def _get_latest_video_url(self):
|
||||
"""Retrieve the latest video file from the customized Yi FTP server."""
|
||||
from aioftp import Client, StatusCodeError
|
||||
|
||||
ftp = Client()
|
||||
ftp = Client(loop=self.hass.loop)
|
||||
try:
|
||||
await ftp.connect(self.host)
|
||||
await ftp.login(self.user, self.passwd)
|
||||
self._ftp = ftp
|
||||
except StatusCodeError as err:
|
||||
raise PlatformNotReady(err)
|
||||
|
||||
async def _get_latest_video_url(self):
|
||||
"""Retrieve the latest video file from the customized Yi FTP server."""
|
||||
from aioftp import StatusCodeError
|
||||
|
||||
try:
|
||||
await self._ftp.change_directory(self.path)
|
||||
await ftp.change_directory(self.path)
|
||||
dirs = []
|
||||
for path, attrs in await self._ftp.list():
|
||||
for path, attrs in await ftp.list():
|
||||
if attrs['type'] == 'dir' and '.' not in str(path):
|
||||
dirs.append(path)
|
||||
latest_dir = dirs[-1]
|
||||
await self._ftp.change_directory(latest_dir)
|
||||
await ftp.change_directory(latest_dir)
|
||||
|
||||
videos = []
|
||||
for path, _ in await self._ftp.list():
|
||||
for path, _ in await ftp.list():
|
||||
videos.append(path)
|
||||
if not videos:
|
||||
_LOGGER.info('Video folder "%s" empty; delaying', latest_dir)
|
||||
return None
|
||||
|
||||
await ftp.quit()
|
||||
|
||||
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
|
||||
self.user, self.passwd, self.host, self.port, self.path,
|
||||
latest_dir, videos[-1])
|
||||
|
||||
15
homeassistant/components/cast/.translations/ca.json
Normal file
15
homeassistant/components/cast/.translations/ca.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Voleu configurar Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/cs.json
Normal file
15
homeassistant/components/cast/.translations/cs.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "V s\u00edti nebyly nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed Google Cast.",
|
||||
"single_instance_allowed": "Pouze jedin\u00e1 konfigurace Google Cast je nezbytn\u00e1."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Chcete nastavit Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
14
homeassistant/components/cast/.translations/de.json
Normal file
14
homeassistant/components/cast/.translations/de.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "M\u00f6chten Sie Google Cast einrichten?",
|
||||
"title": ""
|
||||
}
|
||||
},
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
14
homeassistant/components/cast/.translations/hu.json
Normal file
14
homeassistant/components/cast/.translations/hu.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/it.json
Normal file
15
homeassistant/components/cast/.translations/it.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Nessun dispositivo Google Cast trovato in rete.",
|
||||
"single_instance_allowed": "\u00c8 necessaria una sola configurazione di Google Cast."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Vuoi configurare Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/ko.json
Normal file
15
homeassistant/components/cast/.translations/ko.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Googgle Cast \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
|
||||
"single_instance_allowed": "Google Cast\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Google Cast\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/lb.json
Normal file
15
homeassistant/components/cast/.translations/lb.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Keng Google Cast Apparater am Netzwierk fonnt.",
|
||||
"single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Google Cast ass n\u00e9ideg."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Soll Google Cast konfigur\u00e9iert ginn?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/nl.json
Normal file
15
homeassistant/components/cast/.translations/nl.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Geen Google Cast-apparaten gevonden op het netwerk.",
|
||||
"single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Google Cast nodig."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Wilt u Google Cast instellen?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/no.json
Normal file
15
homeassistant/components/cast/.translations/no.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.",
|
||||
"single_instance_allowed": "Kun en enkelt konfigurasjon av Google Cast er n\u00f8dvendig."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "\u00d8nsker du \u00e5 sette opp Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/pl.json
Normal file
15
homeassistant/components/cast/.translations/pl.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Google Cast.",
|
||||
"single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Google Cast."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Czy chcesz skonfigurowa\u0107 Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/ru.json
Normal file
15
homeassistant/components/cast/.translations/ru.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Google Cast \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
|
||||
"single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Google Cast."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/sl.json
Normal file
15
homeassistant/components/cast/.translations/sl.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "V omre\u017eju niso najdene naprave Google Cast.",
|
||||
"single_instance_allowed": "Potrebna je samo ena konfiguracija Google Cast-a."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Ali \u017eelite nastaviti Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/sv.json
Normal file
15
homeassistant/components/cast/.translations/sv.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Inga Google Cast-enheter hittades i n\u00e4tverket.",
|
||||
"single_instance_allowed": "Endast en enda konfiguration av Google Cast \u00e4r n\u00f6dv\u00e4ndig."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Vill du konfigurera Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/vi.json
Normal file
15
homeassistant/components/cast/.translations/vi.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Google Cast n\u00e0o tr\u00ean m\u1ea1ng.",
|
||||
"single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Google Cast l\u00e0 \u0111\u1ee7."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Google Cast kh\u00f4ng?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/zh-Hans.json
Normal file
15
homeassistant/components/cast/.translations/zh-Hans.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Google Cast \u8bbe\u5907\u3002",
|
||||
"single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Google Cast \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002"
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/zh-Hant.json
Normal file
15
homeassistant/components/cast/.translations/zh-Hant.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002",
|
||||
"single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002"
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "\u662f\u5426\u8981\u8a2d\u5b9a Google Cast\uff1f",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
@@ -470,7 +470,6 @@ async def async_unload_entry(hass, entry):
|
||||
class ClimateDevice(Entity):
|
||||
"""Representation of a climate device."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
|
||||
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
# pylint: disable=import-error, no-name-in-module
|
||||
# pylint: disable=import-error
|
||||
class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Representation of an eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
|
||||
0
homeassistant/components/climate/fritzbox.py
Executable file → Normal file
0
homeassistant/components/climate/fritzbox.py
Executable file → Normal file
@@ -263,7 +263,6 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
|
||||
@@ -273,7 +272,6 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the heatmiser thermostat."""
|
||||
from heatmiserV3 import heatmiser, connection
|
||||
|
||||
130
homeassistant/components/climate/homekit_controller.py
Normal file
130
homeassistant/components/climate/homekit_controller.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Support for Homekit climate devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.homekit_controller/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.homekit_controller import (
|
||||
HomeKitEntity, KNOWN_ACCESSORIES)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_OFF, ATTR_TEMPERATURE
|
||||
|
||||
DEPENDENCIES = ['homekit_controller']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Map of Homekit operation modes to hass modes
|
||||
MODE_HOMEKIT_TO_HASS = {
|
||||
0: STATE_OFF,
|
||||
1: STATE_HEAT,
|
||||
2: STATE_COOL,
|
||||
}
|
||||
|
||||
# Map of hass operation modes to homekit modes
|
||||
MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Homekit climate."""
|
||||
if discovery_info is not None:
|
||||
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
|
||||
add_devices([HomeKitClimateDevice(accessory, discovery_info)], True)
|
||||
|
||||
|
||||
class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
|
||||
"""Representation of a Homekit climate device."""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Initialise the device."""
|
||||
super().__init__(*args)
|
||||
self._state = None
|
||||
self._current_mode = None
|
||||
self._valid_modes = []
|
||||
self._current_temp = None
|
||||
self._target_temp = None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise device state with Home Assistant."""
|
||||
# pylint: disable=import-error
|
||||
from homekit import CharacteristicsTypes as ctypes
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
if ctype == ctypes.HEATING_COOLING_CURRENT:
|
||||
self._state = MODE_HOMEKIT_TO_HASS.get(
|
||||
characteristic['value'])
|
||||
if ctype == ctypes.HEATING_COOLING_TARGET:
|
||||
self._chars['target_mode'] = characteristic['iid']
|
||||
self._features |= SUPPORT_OPERATION_MODE
|
||||
self._current_mode = MODE_HOMEKIT_TO_HASS.get(
|
||||
characteristic['value'])
|
||||
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
|
||||
mode) for mode in characteristic['valid-values']]
|
||||
elif ctype == ctypes.TEMPERATURE_CURRENT:
|
||||
self._current_temp = characteristic['value']
|
||||
elif ctype == ctypes.TEMPERATURE_TARGET:
|
||||
self._chars['target_temp'] = characteristic['iid']
|
||||
self._features |= SUPPORT_TARGET_TEMPERATURE
|
||||
self._target_temp = characteristic['value']
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['target_temp'],
|
||||
'value': temp}]
|
||||
self.put_characteristics(characteristics)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['target_mode'],
|
||||
'value': MODE_HASS_TO_HOMEKIT[operation_mode]}]
|
||||
self.put_characteristics(characteristics)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
# If the device reports its operating mode as off, it sometimes doesn't
|
||||
# report a new state.
|
||||
if self._current_mode == STATE_OFF:
|
||||
return STATE_OFF
|
||||
|
||||
if self._state == STATE_OFF and self._current_mode != STATE_OFF:
|
||||
return STATE_IDLE
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temp
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._valid_modes
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return self._features
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
@@ -12,8 +12,8 @@ from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_MANUAL)
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,12 +30,14 @@ HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()}
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP climate devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP climate from a config entry."""
|
||||
from homematicip.group import HeatingGroup
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.groups:
|
||||
if isinstance(device, HeatingGroup):
|
||||
|
||||
@@ -129,6 +129,9 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT climate devices."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
template_keys = (
|
||||
CONF_POWER_STATE_TEMPLATE,
|
||||
CONF_MODE_STATE_TEMPLATE,
|
||||
@@ -635,11 +638,9 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
# pylint: disable=no-member
|
||||
return self._min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
return self._max_temp
|
||||
|
||||
@@ -26,9 +26,8 @@ DICT_MYS_TO_HA = {
|
||||
'Off': STATE_OFF,
|
||||
}
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
FAN_LIST = ['Auto', 'Min', 'Normal', 'Max']
|
||||
OPERATION_LIST = [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT]
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@@ -39,13 +38,24 @@ async def async_setup_platform(
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice):
|
||||
"""Representation of a MySensors HVAC."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
features = SUPPORT_OPERATION_MODE
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_HVAC_SPEED in self._values:
|
||||
features = features | SUPPORT_FAN_MODE
|
||||
if (set_req.V_HVAC_SETPOINT_COOL in self._values and
|
||||
set_req.V_HVAC_SETPOINT_HEAT in self._values):
|
||||
features = (
|
||||
features | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
else:
|
||||
features = features | SUPPORT_TARGET_TEMPERATURE
|
||||
return features
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
@@ -103,7 +113,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT]
|
||||
return OPERATION_LIST
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
@@ -113,7 +123,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return ['Auto', 'Min', 'Normal', 'Max']
|
||||
return FAN_LIST
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
||||
@@ -5,15 +5,15 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
|
||||
DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,6 +32,15 @@ DEVICE_MAPPINGS = {
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
|
||||
}
|
||||
|
||||
STATE_MAPPINGS = {
|
||||
'Off': STATE_OFF,
|
||||
'Heat': STATE_HEAT,
|
||||
'Heat Mode': STATE_HEAT,
|
||||
'Heat (Default)': STATE_HEAT,
|
||||
'Cool': STATE_COOL,
|
||||
'Auto': STATE_AUTO,
|
||||
}
|
||||
|
||||
|
||||
def get_device(hass, values, **kwargs):
|
||||
"""Create Z-Wave entity device."""
|
||||
@@ -49,6 +58,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
self._operation_list = None
|
||||
self._operation_mapping = None
|
||||
self._operating_state = None
|
||||
self._current_fan_mode = None
|
||||
self._fan_list = None
|
||||
@@ -87,10 +97,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Handle the data changes for node values."""
|
||||
# Operation Mode
|
||||
if self.values.mode:
|
||||
self._current_operation = self.values.mode.data
|
||||
self._operation_list = []
|
||||
self._operation_mapping = {}
|
||||
operation_list = self.values.mode.data_items
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
for mode in operation_list:
|
||||
ha_mode = STATE_MAPPINGS.get(mode)
|
||||
if ha_mode and ha_mode not in self._operation_mapping:
|
||||
self._operation_mapping[ha_mode] = mode
|
||||
self._operation_list.append(ha_mode)
|
||||
continue
|
||||
self._operation_list.append(mode)
|
||||
current_mode = self.values.mode.data
|
||||
self._current_operation = next(
|
||||
(key for key, value in self._operation_mapping.items()
|
||||
if value == current_mode), current_mode)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s", self._current_operation)
|
||||
|
||||
@@ -206,7 +227,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
if self.values.mode:
|
||||
self.values.mode.data = operation_mode
|
||||
self.values.mode.data = self._operation_mapping.get(
|
||||
operation_mode, operation_mode)
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
|
||||
77
homeassistant/components/cloudflare.py
Normal file
77
homeassistant/components/cloudflare.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Update the IP addresses of your Cloudflare DNS records.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/cloudflare/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
|
||||
REQUIREMENTS = ['pycfdns==0.0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RECORDS = 'records'
|
||||
|
||||
DOMAIN = 'cloudflare'
|
||||
|
||||
INTERVAL = timedelta(minutes=60)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_EMAIL): cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_ZONE): cv.string,
|
||||
vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Cloudflare component."""
|
||||
from pycfdns import CloudflareUpdater
|
||||
|
||||
cfupdate = CloudflareUpdater()
|
||||
email = config[DOMAIN][CONF_EMAIL]
|
||||
key = config[DOMAIN][CONF_API_KEY]
|
||||
zone = config[DOMAIN][CONF_ZONE]
|
||||
records = config[DOMAIN][CONF_RECORDS]
|
||||
|
||||
def update_records_interval(now):
|
||||
"""Set up recurring update."""
|
||||
_update_cloudflare(cfupdate, email, key, zone, records)
|
||||
|
||||
def update_records_service(now):
|
||||
"""Set up service for manual trigger."""
|
||||
_update_cloudflare(cfupdate, email, key, zone, records)
|
||||
|
||||
track_time_interval(hass, update_records_interval, INTERVAL)
|
||||
hass.services.register(
|
||||
DOMAIN, 'update_records', update_records_service)
|
||||
return True
|
||||
|
||||
|
||||
def _update_cloudflare(cfupdate, email, key, zone, records):
|
||||
"""Update DNS records for a given zone."""
|
||||
_LOGGER.debug("Starting update for zone %s", zone)
|
||||
|
||||
headers = cfupdate.set_header(email, key)
|
||||
_LOGGER.debug("Header data defined as: %s", headers)
|
||||
|
||||
zoneid = cfupdate.get_zoneID(headers, zone)
|
||||
_LOGGER.debug("Zone ID is set to: %s", zoneid)
|
||||
|
||||
update_records = cfupdate.get_recordInfo(headers, zoneid, zone, records)
|
||||
_LOGGER.debug("Records: %s", update_records)
|
||||
|
||||
result = cfupdate.update_records(headers, zoneid, update_records)
|
||||
_LOGGER.debug("Update for zone %s is complete", zone)
|
||||
|
||||
if result is not True:
|
||||
_LOGGER.warning(result)
|
||||
@@ -49,6 +49,10 @@ async def async_setup(hass, config):
|
||||
|
||||
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
|
||||
|
||||
if hass.auth.active:
|
||||
tasks.append(setup_panel('auth'))
|
||||
tasks.append(setup_panel('auth_provider_homeassistant'))
|
||||
|
||||
for panel_name in ON_DEMAND:
|
||||
if panel_name in hass.config.components:
|
||||
tasks.append(setup_panel(panel_name))
|
||||
|
||||
113
homeassistant/components/config/auth.py
Normal file
113
homeassistant/components/config/auth.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Offer API to configure Home Assistant auth."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import websocket_api
|
||||
|
||||
|
||||
WS_TYPE_LIST = 'config/auth/list'
|
||||
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_LIST,
|
||||
})
|
||||
|
||||
WS_TYPE_DELETE = 'config/auth/delete'
|
||||
SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_DELETE,
|
||||
vol.Required('user_id'): str,
|
||||
})
|
||||
|
||||
WS_TYPE_CREATE = 'config/auth/create'
|
||||
SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CREATE,
|
||||
vol.Required('name'): str,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Home Assistant views."""
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_LIST, websocket_list,
|
||||
SCHEMA_WS_LIST
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DELETE, websocket_delete,
|
||||
SCHEMA_WS_DELETE
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CREATE, websocket_create,
|
||||
SCHEMA_WS_CREATE
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_owner
|
||||
def websocket_list(hass, connection, msg):
|
||||
"""Return a list of users."""
|
||||
async def send_users():
|
||||
"""Send users."""
|
||||
result = [_user_info(u) for u in await hass.auth.async_get_users()]
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id'], result))
|
||||
|
||||
hass.async_add_job(send_users())
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_owner
|
||||
def websocket_delete(hass, connection, msg):
|
||||
"""Delete a user."""
|
||||
async def delete_user():
|
||||
"""Delete user."""
|
||||
if msg['user_id'] == connection.request.get('hass_user').id:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'no_delete_self',
|
||||
'Unable to delete your own account'))
|
||||
return
|
||||
|
||||
user = await hass.auth.async_get_user(msg['user_id'])
|
||||
|
||||
if not user:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'not_found', 'User not found'))
|
||||
return
|
||||
|
||||
await hass.auth.async_remove_user(user)
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id']))
|
||||
|
||||
hass.async_add_job(delete_user())
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_owner
|
||||
def websocket_create(hass, connection, msg):
|
||||
"""Create a user."""
|
||||
async def create_user():
|
||||
"""Create a user."""
|
||||
user = await hass.auth.async_create_user(msg['name'])
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id'], {
|
||||
'user': _user_info(user)
|
||||
}))
|
||||
|
||||
hass.async_add_job(create_user())
|
||||
|
||||
|
||||
def _user_info(user):
|
||||
"""Format a user."""
|
||||
return {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'system_generated': user.system_generated,
|
||||
'credentials': [
|
||||
{
|
||||
'type': c.auth_provider_type,
|
||||
} for c in user.credentials
|
||||
]
|
||||
}
|
||||
120
homeassistant/components/config/auth_provider_homeassistant.py
Normal file
120
homeassistant/components/config/auth_provider_homeassistant.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Offer API to configure the Home Assistant auth provider."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.providers import homeassistant as auth_ha
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import websocket_api
|
||||
|
||||
|
||||
WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create'
|
||||
SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CREATE,
|
||||
vol.Required('user_id'): str,
|
||||
vol.Required('username'): str,
|
||||
vol.Required('password'): str,
|
||||
})
|
||||
|
||||
WS_TYPE_DELETE = 'config/auth_provider/homeassistant/delete'
|
||||
SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_DELETE,
|
||||
vol.Required('username'): str,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Home Assistant views."""
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CREATE, websocket_create,
|
||||
SCHEMA_WS_CREATE
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DELETE, websocket_delete,
|
||||
SCHEMA_WS_DELETE
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _get_provider(hass):
|
||||
"""Get homeassistant auth provider."""
|
||||
for prv in hass.auth.auth_providers:
|
||||
if prv.type == 'homeassistant':
|
||||
return prv
|
||||
|
||||
raise RuntimeError('Provider not found')
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_owner
|
||||
def websocket_create(hass, connection, msg):
|
||||
"""Create credentials and attach to a user."""
|
||||
async def create_creds():
|
||||
"""Create credentials."""
|
||||
provider = _get_provider(hass)
|
||||
await provider.async_initialize()
|
||||
|
||||
user = await hass.auth.async_get_user(msg['user_id'])
|
||||
|
||||
if user is None:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'not_found', 'User not found'))
|
||||
return
|
||||
|
||||
if user.system_generated:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'system_generated',
|
||||
'Cannot add credentials to a system generated user.'))
|
||||
return
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(
|
||||
provider.data.add_auth, msg['username'], msg['password'])
|
||||
except auth_ha.InvalidUser:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'username_exists', 'Username already exists'))
|
||||
return
|
||||
|
||||
credentials = await provider.async_get_or_create_credentials({
|
||||
'username': msg['username']
|
||||
})
|
||||
await hass.auth.async_link_user(user, credentials)
|
||||
|
||||
await provider.data.async_save()
|
||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id']))
|
||||
|
||||
hass.async_add_job(create_creds())
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_owner
|
||||
def websocket_delete(hass, connection, msg):
|
||||
"""Delete username and related credential."""
|
||||
async def delete_creds():
|
||||
"""Delete user credentials."""
|
||||
provider = _get_provider(hass)
|
||||
await provider.async_initialize()
|
||||
|
||||
credentials = await provider.async_get_or_create_credentials({
|
||||
'username': msg['username']
|
||||
})
|
||||
|
||||
# if not new, an existing credential exists.
|
||||
# Removing the credential will also remove the auth.
|
||||
if not credentials.is_new:
|
||||
await hass.auth.async_remove_credentials(credentials)
|
||||
|
||||
connection.to_write.put_nowait(
|
||||
websocket_api.result_message(msg['id']))
|
||||
return
|
||||
|
||||
try:
|
||||
provider.data.async_remove_auth(msg['username'])
|
||||
await provider.data.async_save()
|
||||
except auth_ha.InvalidUser:
|
||||
connection.to_write.put_nowait(websocket_api.error_message(
|
||||
msg['id'], 'auth_not_found', 'Given username was not found.'))
|
||||
return
|
||||
|
||||
connection.to_write.put_nowait(
|
||||
websocket_api.result_message(msg['id']))
|
||||
|
||||
hass.async_add_job(delete_creds())
|
||||
@@ -198,7 +198,6 @@ async def async_setup(hass, config):
|
||||
class CoverDevice(Entity):
|
||||
"""Representation a cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
@@ -24,7 +24,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class DemoCover(CoverDevice):
|
||||
"""Representation of a demo cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, name, position=None, tilt_position=None,
|
||||
device_class=None, supported_features=None):
|
||||
"""Initialize the cover."""
|
||||
|
||||
@@ -73,7 +73,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class GaradgetCover(CoverDevice):
|
||||
"""Representation of a Garadget cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, args):
|
||||
"""Initialize the cover."""
|
||||
self.particle_url = 'https://api.particle.io'
|
||||
|
||||
0
homeassistant/components/cover/group.py
Executable file → Normal file
0
homeassistant/components/cover/group.py
Executable file → Normal file
@@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.isy994/
|
||||
"""
|
||||
import logging
|
||||
from typing import Callable # noqa
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, DOMAIN
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Lutron Caseta shades.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.lutron_caseta/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
@@ -18,8 +17,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['lutron_caseta']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Lutron Caseta shades as a cover device."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
@@ -49,25 +48,21 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
"""Return the current position of cover."""
|
||||
return self._state['current_state']
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 0)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 100)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the shade to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
self._smartbridge.set_value(self._device_id, position)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Call when forcing a refresh of the device."""
|
||||
self._state = self._smartbridge.get_device_by_id(self._device_id)
|
||||
_LOGGER.debug(self._state)
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for MQTT cover devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -93,8 +92,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the MQTT Cover."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
@@ -174,10 +173,9 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
self._position_topic = position_topic
|
||||
self._set_position_template = set_position_template
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe MQTT events."""
|
||||
yield from super().async_added_to_hass()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def tilt_updated(topic, payload, qos):
|
||||
@@ -218,7 +216,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
else:
|
||||
yield from mqtt.async_subscribe(
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._state_topic,
|
||||
state_message_received, self._qos)
|
||||
|
||||
@@ -227,7 +225,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
else:
|
||||
self._tilt_optimistic = False
|
||||
self._tilt_value = STATE_UNKNOWN
|
||||
yield from mqtt.async_subscribe(
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._tilt_status_topic, tilt_updated, self._qos)
|
||||
|
||||
@property
|
||||
@@ -278,8 +276,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
|
||||
return supported_features
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -292,8 +289,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
self._state = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -306,8 +302,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
self._state = True
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the device.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -316,8 +311,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
self.hass, self._command_topic, self._payload_stop, self._qos,
|
||||
self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover_tilt(self, **kwargs):
|
||||
async def async_open_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover open."""
|
||||
mqtt.async_publish(self.hass, self._tilt_command_topic,
|
||||
self._tilt_open_position, self._qos,
|
||||
@@ -326,8 +320,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
self._tilt_value = self._tilt_open_position
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover_tilt(self, **kwargs):
|
||||
async def async_close_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover closed."""
|
||||
mqtt.async_publish(self.hass, self._tilt_command_topic,
|
||||
self._tilt_closed_position, self._qos,
|
||||
@@ -336,8 +329,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
self._tilt_value = self._tilt_closed_position
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION not in kwargs:
|
||||
return
|
||||
@@ -350,8 +342,7 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
mqtt.async_publish(self.hass, self._tilt_command_topic,
|
||||
level, self._qos, self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
|
||||
@@ -17,7 +17,7 @@ async def async_setup_platform(
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice):
|
||||
"""Representation of the value of a MySensors Cover child node."""
|
||||
|
||||
@property
|
||||
|
||||
@@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class OpenGarageCover(CoverDevice):
|
||||
"""Representation of a OpenGarage cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, args):
|
||||
"""Initialize the cover."""
|
||||
self.opengarage_url = 'http://{}:{}'.format(
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Rflink Cover devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.rflink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -79,8 +78,8 @@ def devices_from_config(domain_config, hass=None):
|
||||
return devices
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Rflink cover platform."""
|
||||
async_add_devices(devices_from_config(config, hass))
|
||||
|
||||
|
||||
@@ -81,7 +81,11 @@ class TahomaCover(TahomaDevice, CoverDevice):
|
||||
self.apply_action('setPosition', 'secured')
|
||||
elif self.tahoma_device.type in \
|
||||
('rts:BlindRTSComponent',
|
||||
'io:ExteriorVenetianBlindIOComponent'):
|
||||
'io:ExteriorVenetianBlindIOComponent',
|
||||
'rts:VenetianBlindRTSComponent',
|
||||
'rts:DualCurtainRTSComponent',
|
||||
'rts:ExteriorVenetianBlindRTSComponent',
|
||||
'rts:BlindRTSComponent'):
|
||||
self.apply_action('my')
|
||||
else:
|
||||
self.apply_action('stopIdentify')
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for covers which integrate with other components.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.template/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -72,8 +71,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Template cover."""
|
||||
covers = []
|
||||
|
||||
@@ -199,8 +198,7 @@ class CoverTemplate(CoverDevice):
|
||||
if self._entity_picture_template is not None:
|
||||
self._entity_picture_template.hass = self.hass
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@callback
|
||||
def template_cover_state_listener(entity, old_state, new_state):
|
||||
@@ -277,70 +275,62 @@ class CoverTemplate(CoverDevice):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
if self._open_script:
|
||||
yield from self._open_script.async_run()
|
||||
await self._open_script.async_run()
|
||||
elif self._position_script:
|
||||
yield from self._position_script.async_run({"position": 100})
|
||||
await self._position_script.async_run({"position": 100})
|
||||
if self._optimistic:
|
||||
self._position = 100
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
if self._close_script:
|
||||
yield from self._close_script.async_run()
|
||||
await self._close_script.async_run()
|
||||
elif self._position_script:
|
||||
yield from self._position_script.async_run({"position": 0})
|
||||
await self._position_script.async_run({"position": 0})
|
||||
if self._optimistic:
|
||||
self._position = 0
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
if self._stop_script:
|
||||
yield from self._stop_script.async_run()
|
||||
await self._stop_script.async_run()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
self._position = kwargs[ATTR_POSITION]
|
||||
yield from self._position_script.async_run(
|
||||
await self._position_script.async_run(
|
||||
{"position": self._position})
|
||||
if self._optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover_tilt(self, **kwargs):
|
||||
async def async_open_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover open."""
|
||||
self._tilt_value = 100
|
||||
yield from self._tilt_script.async_run({"tilt": self._tilt_value})
|
||||
await self._tilt_script.async_run({"tilt": self._tilt_value})
|
||||
if self._tilt_optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover_tilt(self, **kwargs):
|
||||
async def async_close_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover closed."""
|
||||
self._tilt_value = 0
|
||||
yield from self._tilt_script.async_run(
|
||||
await self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value})
|
||||
if self._tilt_optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
self._tilt_value = kwargs[ATTR_TILT_POSITION]
|
||||
yield from self._tilt_script.async_run({"tilt": self._tilt_value})
|
||||
await self._tilt_script.async_run({"tilt": self._tilt_value})
|
||||
if self._tilt_optimistic:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
if self._template is not None:
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.velbus/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -70,15 +69,14 @@ class VelbusCover(CoverDevice):
|
||||
self._open_channel = open_channel
|
||||
self._close_channel = close_channel
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Add listener for Velbus messages on bus."""
|
||||
def _init_velbus():
|
||||
"""Initialize Velbus on startup."""
|
||||
self._velbus.subscribe(self._on_message)
|
||||
self.get_status()
|
||||
|
||||
yield from self.hass.async_add_job(_init_velbus)
|
||||
await self.hass.async_add_job(_init_velbus)
|
||||
|
||||
def _on_message(self, message):
|
||||
import velbus
|
||||
|
||||
@@ -4,8 +4,6 @@ Support for Wink Covers.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.wink/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \
|
||||
ATTR_POSITION
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
@@ -21,6 +19,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_id = shade.object_id() + shade.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkCoverDevice(shade, hass)])
|
||||
for shade in pywink.get_shade_groups():
|
||||
_id = shade.object_id() + shade.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkCoverDevice(shade, hass)])
|
||||
for door in pywink.get_garage_doors():
|
||||
_id = door.object_id() + door.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
@@ -30,8 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Representation of a Wink cover device."""
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['cover'].append(self)
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def __init__(self, hass, values, invert_buttons):
|
||||
"""Initialize the Z-Wave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._network = hass.data[zwave.const.DATA_NETWORK]
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u041c\u043e\u0441\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d",
|
||||
"no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ",
|
||||
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ"
|
||||
},
|
||||
|
||||
33
homeassistant/components/deconz/.translations/ca.json
Normal file
33
homeassistant/components/deconz/.translations/ca.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "L'enlla\u00e7 ja est\u00e0 configurat",
|
||||
"no_bridges": "No s'han descobert enlla\u00e7os amb deCONZ",
|
||||
"one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia deCONZ"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "No s'ha pogut obtenir una clau API"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Amfitri\u00f3",
|
||||
"port": "Port (predeterminat: '80')"
|
||||
},
|
||||
"title": "Definiu la passarel\u00b7la deCONZ"
|
||||
},
|
||||
"link": {
|
||||
"description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"",
|
||||
"title": "Vincular amb deCONZ"
|
||||
},
|
||||
"options": {
|
||||
"data": {
|
||||
"allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals",
|
||||
"allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ"
|
||||
},
|
||||
"title": "Opcions de configuraci\u00f3 addicionals per deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user