mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
commit
da3366859d
@ -64,6 +64,8 @@ omit =
|
||||
homeassistant/components/cast/*
|
||||
homeassistant/components/*/cast.py
|
||||
|
||||
homeassistant/components/cloudflare.py
|
||||
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.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
|
||||
|
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,670 +0,0 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
import asyncio
|
||||
import binascii
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@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 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 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."""
|
||||
tkn = self._access_tokens.get(token)
|
||||
|
||||
if tkn is None:
|
||||
return None
|
||||
|
||||
if tkn.expired:
|
||||
self._access_tokens.pop(token)
|
||||
return None
|
||||
|
||||
return tkn
|
||||
|
||||
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_or_create_client(self, name, *, redirect_uris=None,
|
||||
no_secret=False):
|
||||
"""Find a client, if not exists, create a new one."""
|
||||
for client in await self._store.async_get_clients():
|
||||
if client.name == name:
|
||||
return 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._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
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_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."""
|
||||
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."""
|
||||
local_user = await self.async_get_user(user.id)
|
||||
if local_user is None:
|
||||
raise ValueError('Invalid user')
|
||||
|
||||
local_client = await self.async_get_client(client_id)
|
||||
if local_client is None:
|
||||
raise ValueError('Invalid client_id')
|
||||
|
||||
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_clients(self):
|
||||
"""Return all clients."""
|
||||
if self._clients is None:
|
||||
await self.async_load()
|
||||
|
||||
return list(self._clients.values())
|
||||
|
||||
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."""
|
||||
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
|
||||
|
||||
if data is None:
|
||||
self._users = {}
|
||||
self._clients = {}
|
||||
return
|
||||
|
||||
users = {
|
||||
user_dict['id']: User(**user_dict) for user_dict in data['users']
|
||||
}
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(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 = {}
|
||||
|
||||
for rt_dict in data['refresh_tokens']:
|
||||
token = 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 = 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)
|
||||
|
||||
clients = {
|
||||
cl_dict['id']: Client(**cl_dict) for cl_dict in data['clients']
|
||||
}
|
||||
|
||||
self._users = users
|
||||
self._clients = clients
|
||||
|
||||
async def async_save(self):
|
||||
"""Save users."""
|
||||
users = [
|
||||
{
|
||||
'id': user.id,
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'name': user.name,
|
||||
}
|
||||
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
|
||||
]
|
||||
|
||||
clients = [
|
||||
{
|
||||
'id': client.id,
|
||||
'name': client.name,
|
||||
'secret': client.secret,
|
||||
'redirect_uris': client.redirect_uris,
|
||||
}
|
||||
for client in self._clients.values()
|
||||
]
|
||||
|
||||
data = {
|
||||
'users': users,
|
||||
'clients': clients,
|
||||
'credentials': credentials,
|
||||
'access_tokens': access_tokens,
|
||||
'refresh_tokens': refresh_tokens,
|
||||
}
|
||||
|
||||
await self._store.async_save(data, delay=1)
|
243
homeassistant/auth/__init__.py
Normal file
243
homeassistant/auth/__init__.py
Normal file
@ -0,0 +1,243 @@
|
||||
"""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'),
|
||||
is_active=info.get('is_active', False)
|
||||
)
|
||||
|
||||
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)
|
143
homeassistant/auth/providers/__init__.py
Normal file
143
homeassistant/auth/providers/__init__.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""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.
|
||||
|
||||
Values to populate:
|
||||
- name: string
|
||||
- is_active: boolean
|
||||
"""
|
||||
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.auth.util import generate_secret
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_provider.homeassistant'
|
||||
|
||||
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
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.')
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id)
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
@ -43,7 +57,7 @@ class Data:
|
||||
|
||||
if data is None:
|
||||
data = {
|
||||
'salt': auth.generate_secret(),
|
||||
'salt': generate_secret(),
|
||||
'users': []
|
||||
}
|
||||
|
||||
@ -85,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
|
||||
|
||||
@ -95,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.
|
||||
"""
|
||||
@ -112,22 +140,33 @@ class Data:
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
|
||||
@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."""
|
||||
data = Data(self.hass)
|
||||
await data.async_load()
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
data.validate_login, username, password)
|
||||
self.data.validate_login, username, password)
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
@ -142,6 +181,25 @@ class HassAuthProvider(auth.AuthProvider):
|
||||
'username': username
|
||||
})
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Get extra info for this credential."""
|
||||
return {
|
||||
'name': credentials.data['username'],
|
||||
'is_active': True,
|
||||
}
|
||||
|
||||
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):
|
||||
"""Handler for the login flow."""
|
@ -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):
|
||||
@ -73,14 +75,16 @@ class ExampleAuthProvider(auth.AuthProvider):
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
username = credentials.data['username']
|
||||
info = {
|
||||
'is_active': True,
|
||||
}
|
||||
|
||||
for user in self.config['users']:
|
||||
if user['username'] == username:
|
||||
return {
|
||||
'name': user.get('name')
|
||||
}
|
||||
info['name'] = user.get('name')
|
||||
break
|
||||
|
||||
return {}
|
||||
return info
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
@ -9,15 +9,18 @@ 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,
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
LEGACY_USER = 'homeassistant'
|
||||
@ -27,8 +30,8 @@ class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('legacy_api_password')
|
||||
class LegacyApiPasswordAuthProvider(auth.AuthProvider):
|
||||
@AUTH_PROVIDERS.register('legacy_api_password')
|
||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
DEFAULT_TITLE = 'Legacy API Password'
|
||||
@ -67,7 +70,10 @@ class LegacyApiPasswordAuthProvider(auth.AuthProvider):
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return {'name': LEGACY_USER}
|
||||
return {
|
||||
'name': LEGACY_USER,
|
||||
'is_active': True,
|
||||
}
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
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:
|
||||
@ -137,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()
|
||||
|
||||
@ -145,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()
|
||||
|
||||
@ -162,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,
|
||||
@ -187,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.
|
||||
@ -204,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,17 @@ 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
|
@ -270,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):
|
||||
@ -438,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],
|
||||
|
@ -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.8']
|
||||
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)
|
||||
|
||||
@ -223,25 +241,32 @@ class GrantTokenView(HomeAssistantView):
|
||||
url = '/auth/token'
|
||||
name = 'api:auth:token'
|
||||
requires_auth = False
|
||||
cors_allowed = True
|
||||
|
||||
def __init__(self, retrieve_credentials):
|
||||
"""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 +286,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 +373,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')
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
})
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
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
|
||||
}
|
0
homeassistant/components/climate/fritzbox.py
Executable file → Normal file
0
homeassistant/components/climate/fritzbox.py
Executable file → Normal file
@ -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):
|
||||
|
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
|
||||
]
|
||||
}
|
174
homeassistant/components/config/auth_provider_homeassistant.py
Normal file
174
homeassistant/components/config/auth_provider_homeassistant.py
Normal file
@ -0,0 +1,174 @@
|
||||
"""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,
|
||||
})
|
||||
|
||||
WS_TYPE_CHANGE_PASSWORD = 'config/auth_provider/homeassistant/change_password'
|
||||
SCHEMA_WS_CHANGE_PASSWORD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CHANGE_PASSWORD,
|
||||
vol.Required('current_password'): str,
|
||||
vol.Required('new_password'): 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
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CHANGE_PASSWORD, websocket_change_password,
|
||||
SCHEMA_WS_CHANGE_PASSWORD
|
||||
)
|
||||
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())
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_change_password(hass, connection, msg):
|
||||
"""Change user password."""
|
||||
async def change_password():
|
||||
"""Change user password."""
|
||||
user = connection.request.get('hass_user')
|
||||
if user is None:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'user_not_found', 'User not found'))
|
||||
return
|
||||
|
||||
provider = _get_provider(hass)
|
||||
await provider.async_initialize()
|
||||
|
||||
username = None
|
||||
for credential in user.credentials:
|
||||
if credential.auth_provider_type == provider.type:
|
||||
username = credential.data['username']
|
||||
break
|
||||
|
||||
if username is None:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'credentials_not_found', 'Credentials not found'))
|
||||
return
|
||||
|
||||
try:
|
||||
await provider.async_validate_login(
|
||||
username, msg['current_password'])
|
||||
except auth_ha.InvalidAuth:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'invalid_password', 'Invalid password'))
|
||||
return
|
||||
|
||||
await hass.async_add_executor_job(
|
||||
provider.data.change_password, username, msg['new_password'])
|
||||
await provider.data.async_save()
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id']))
|
||||
|
||||
hass.async_add_job(change_password())
|
0
homeassistant/components/cover/group.py
Executable file → Normal file
0
homeassistant/components/cover/group.py
Executable file → Normal file
@ -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]
|
||||
|
@ -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
|
||||
@ -34,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)
|
||||
|
||||
|
@ -22,7 +22,7 @@ from .const import (
|
||||
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
|
||||
|
||||
REQUIREMENTS = ['pydeconz==39']
|
||||
REQUIREMENTS = ['pydeconz==42']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -11,3 +11,6 @@ DATA_DECONZ_UNSUB = 'deconz_dispatchers'
|
||||
|
||||
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
|
||||
CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
|
||||
|
||||
ATTR_DARK = 'dark'
|
||||
ATTR_ON = 'on'
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
REQUIREMENTS = ['pexpect==4.6.0']
|
||||
|
||||
_DEVICES_REGEX = re.compile(
|
||||
r'(?P<name>([^\s]+)?)\s+' +
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE,
|
||||
CONF_PROTOCOL)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
REQUIREMENTS = ['pexpect==4.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -311,12 +311,11 @@ class SshConnection(_Connection):
|
||||
|
||||
super().connect()
|
||||
|
||||
def disconnect(self): \
|
||||
# pylint: disable=broad-except
|
||||
def disconnect(self):
|
||||
"""Disconnect the current SSH connection."""
|
||||
try:
|
||||
self._ssh.logout()
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
finally:
|
||||
self._ssh = None
|
||||
@ -379,12 +378,11 @@ class TelnetConnection(_Connection):
|
||||
|
||||
super().connect()
|
||||
|
||||
def disconnect(self): \
|
||||
# pylint: disable=broad-except
|
||||
def disconnect(self):
|
||||
"""Disconnect the current Telnet connection."""
|
||||
try:
|
||||
self._telnet.write('exit\n'.encode('ascii'))
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
super().disconnect()
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
REQUIREMENTS = ['pexpect==4.6.0']
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend({
|
||||
|
@ -66,7 +66,6 @@ class MikrotikScanner(DeviceScanner):
|
||||
|
||||
def connect_to_device(self):
|
||||
"""Connect to Mikrotik method."""
|
||||
# pylint: disable=import-error
|
||||
import librouteros
|
||||
try:
|
||||
self.client = librouteros.connect(
|
||||
|
@ -5,24 +5,22 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tile/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD)
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pytile==1.1.0']
|
||||
REQUIREMENTS = ['pytile==2.0.2']
|
||||
|
||||
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
|
||||
DEFAULT_ICON = 'mdi:bluetooth'
|
||||
DEVICE_TYPES = ['PHONE', 'TILE']
|
||||
|
||||
ATTR_ALTITUDE = 'altitude'
|
||||
@ -34,89 +32,111 @@ ATTR_VOIP_STATE = 'voip_state'
|
||||
|
||||
CONF_SHOW_INACTIVE = 'show_inactive'
|
||||
|
||||
DEFAULT_ICON = 'mdi:bluetooth'
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=2)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_MONITORED_VARIABLES):
|
||||
vol.Optional(CONF_MONITORED_VARIABLES, default=DEVICE_TYPES):
|
||||
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Validate the configuration and return a Tile scanner."""
|
||||
TileDeviceScanner(hass, config, see)
|
||||
return True
|
||||
from pytile import Client
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
config_data = await hass.async_add_job(
|
||||
load_json, hass.config.path(CLIENT_UUID_CONFIG_FILE))
|
||||
if config_data:
|
||||
client = Client(
|
||||
config[CONF_USERNAME],
|
||||
config[CONF_PASSWORD],
|
||||
websession,
|
||||
client_uuid=config_data['client_uuid'])
|
||||
else:
|
||||
client = Client(
|
||||
config[CONF_USERNAME], config[CONF_PASSWORD], websession)
|
||||
|
||||
config_data = {'client_uuid': client.client_uuid}
|
||||
config_saved = await hass.async_add_job(
|
||||
save_json, hass.config.path(CLIENT_UUID_CONFIG_FILE), config_data)
|
||||
if not config_saved:
|
||||
_LOGGER.error('Failed to save the client UUID')
|
||||
|
||||
scanner = TileScanner(
|
||||
client, hass, async_see, config[CONF_MONITORED_VARIABLES],
|
||||
config[CONF_SHOW_INACTIVE])
|
||||
return await scanner.async_init()
|
||||
|
||||
|
||||
class TileDeviceScanner(DeviceScanner):
|
||||
"""Define a device scanner for Tiles."""
|
||||
class TileScanner(object):
|
||||
"""Define an object to retrieve Tile data."""
|
||||
|
||||
def __init__(self, hass, config, see):
|
||||
def __init__(self, client, hass, async_see, types, show_inactive):
|
||||
"""Initialize."""
|
||||
from pytile import Client
|
||||
self._async_see = async_see
|
||||
self._client = client
|
||||
self._hass = hass
|
||||
self._show_inactive = show_inactive
|
||||
self._types = types
|
||||
|
||||
_LOGGER.debug('Received configuration data: %s', config)
|
||||
async def async_init(self):
|
||||
"""Further initialize connection to the Tile servers."""
|
||||
from pytile.errors import TileError
|
||||
|
||||
# Load the client UUID (if it exists):
|
||||
config_data = load_json(hass.config.path(CLIENT_UUID_CONFIG_FILE))
|
||||
if config_data:
|
||||
_LOGGER.debug('Using existing client UUID')
|
||||
self._client = Client(
|
||||
config[CONF_USERNAME],
|
||||
config[CONF_PASSWORD],
|
||||
config_data['client_uuid'])
|
||||
else:
|
||||
_LOGGER.debug('Generating new client UUID')
|
||||
self._client = Client(
|
||||
config[CONF_USERNAME],
|
||||
config[CONF_PASSWORD])
|
||||
try:
|
||||
await self._client.async_init()
|
||||
except TileError as err:
|
||||
_LOGGER.error('Unable to set up Tile scanner: %s', err)
|
||||
return False
|
||||
|
||||
if not save_json(
|
||||
hass.config.path(CLIENT_UUID_CONFIG_FILE),
|
||||
{'client_uuid': self._client.client_uuid}):
|
||||
_LOGGER.error("Failed to save configuration file")
|
||||
await self._async_update()
|
||||
|
||||
_LOGGER.debug('Client UUID: %s', self._client.client_uuid)
|
||||
_LOGGER.debug('User UUID: %s', self._client.user_uuid)
|
||||
async_track_time_interval(
|
||||
self._hass, self._async_update, DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
self._show_inactive = config.get(CONF_SHOW_INACTIVE)
|
||||
self._types = config.get(CONF_MONITORED_VARIABLES)
|
||||
return True
|
||||
|
||||
self.devices = {}
|
||||
self.see = see
|
||||
async def _async_update(self, now=None):
|
||||
"""Update info from Tile."""
|
||||
from pytile.errors import SessionExpiredError, TileError
|
||||
|
||||
track_utc_time_change(
|
||||
hass, self._update_info, second=range(0, 60, 30))
|
||||
_LOGGER.debug('Updating Tile data')
|
||||
|
||||
self._update_info()
|
||||
try:
|
||||
await self._client.asayn_init()
|
||||
tiles = await self._client.tiles.all(
|
||||
whitelist=self._types, show_inactive=self._show_inactive)
|
||||
except SessionExpiredError:
|
||||
_LOGGER.info('Session expired; trying again shortly')
|
||||
return
|
||||
except TileError as err:
|
||||
_LOGGER.error('There was an error while updating: %s', err)
|
||||
return
|
||||
|
||||
def _update_info(self, now=None) -> None:
|
||||
"""Update the device info."""
|
||||
self.devices = self._client.get_tiles(
|
||||
type_whitelist=self._types, show_inactive=self._show_inactive)
|
||||
|
||||
if not self.devices:
|
||||
if not tiles:
|
||||
_LOGGER.warning('No Tiles found')
|
||||
return
|
||||
|
||||
for dev in self.devices:
|
||||
dev_id = 'tile_{0}'.format(slugify(dev['name']))
|
||||
lat = dev['tileState']['latitude']
|
||||
lon = dev['tileState']['longitude']
|
||||
|
||||
attrs = {
|
||||
ATTR_ALTITUDE: dev['tileState']['altitude'],
|
||||
ATTR_CONNECTION_STATE: dev['tileState']['connection_state'],
|
||||
ATTR_IS_DEAD: dev['is_dead'],
|
||||
ATTR_IS_LOST: dev['tileState']['is_lost'],
|
||||
ATTR_RING_STATE: dev['tileState']['ring_state'],
|
||||
ATTR_VOIP_STATE: dev['tileState']['voip_state'],
|
||||
}
|
||||
|
||||
self.see(
|
||||
dev_id=dev_id,
|
||||
gps=(lat, lon),
|
||||
attributes=attrs,
|
||||
icon=DEFAULT_ICON
|
||||
)
|
||||
for tile in tiles:
|
||||
await self._async_see(
|
||||
dev_id='tile_{0}'.format(slugify(tile['name'])),
|
||||
gps=(
|
||||
tile['tileState']['latitude'],
|
||||
tile['tileState']['longitude']
|
||||
),
|
||||
attributes={
|
||||
ATTR_ALTITUDE: tile['tileState']['altitude'],
|
||||
ATTR_CONNECTION_STATE:
|
||||
tile['tileState']['connection_state'],
|
||||
ATTR_IS_DEAD: tile['is_dead'],
|
||||
ATTR_IS_LOST: tile['tileState']['is_lost'],
|
||||
ATTR_RING_STATE: tile['tileState']['ring_state'],
|
||||
ATTR_VOIP_STATE: tile['tileState']['voip_state'],
|
||||
},
|
||||
icon=DEFAULT_ICON)
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
|
||||
CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
REQUIREMENTS = ['pexpect==4.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -64,7 +64,7 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
|
||||
station_info = await self.hass.async_add_job(self.device.status)
|
||||
_LOGGER.debug("Got new station info: %s", station_info)
|
||||
|
||||
for device in station_info['mat']:
|
||||
for device in station_info.associated_stations:
|
||||
devices.append(device['mac'])
|
||||
|
||||
except DeviceException as ex:
|
||||
|
@ -99,7 +99,8 @@ async def async_handle_message(hass, message):
|
||||
return None
|
||||
|
||||
action = req.get('action', '')
|
||||
parameters = req.get('parameters')
|
||||
parameters = req.get('parameters').copy()
|
||||
parameters["dialogflow_query"] = message
|
||||
dialogflow_response = DialogflowResponse(parameters)
|
||||
|
||||
if action == "":
|
||||
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/emulated_hue/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import util
|
||||
@ -13,7 +14,6 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.components.http import REQUIREMENTS # NOQA
|
||||
from homeassistant.components.http import HomeAssistantHTTP
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -85,28 +85,17 @@ def setup(hass, yaml_config):
|
||||
"""Activate the emulated_hue component."""
|
||||
config = Config(hass, yaml_config.get(DOMAIN, {}))
|
||||
|
||||
server = HomeAssistantHTTP(
|
||||
hass,
|
||||
server_host=config.host_ip_addr,
|
||||
server_port=config.listen_port,
|
||||
api_password=None,
|
||||
ssl_certificate=None,
|
||||
ssl_peer_certificate=None,
|
||||
ssl_key=None,
|
||||
cors_origins=None,
|
||||
use_x_forwarded_for=False,
|
||||
trusted_proxies=[],
|
||||
trusted_networks=[],
|
||||
login_threshold=0,
|
||||
is_ban_enabled=False
|
||||
)
|
||||
app = web.Application()
|
||||
app['hass'] = hass
|
||||
handler = None
|
||||
server = None
|
||||
|
||||
server.register_view(DescriptionXmlView(config))
|
||||
server.register_view(HueUsernameView)
|
||||
server.register_view(HueAllLightsStateView(config))
|
||||
server.register_view(HueOneLightStateView(config))
|
||||
server.register_view(HueOneLightChangeView(config))
|
||||
server.register_view(HueGroupView(config))
|
||||
DescriptionXmlView(config).register(app, app.router)
|
||||
HueUsernameView().register(app, app.router)
|
||||
HueAllLightsStateView(config).register(app, app.router)
|
||||
HueOneLightStateView(config).register(app, app.router)
|
||||
HueOneLightChangeView(config).register(app, app.router)
|
||||
HueGroupView(config).register(app, app.router)
|
||||
|
||||
upnp_listener = UPNPResponderThread(
|
||||
config.host_ip_addr, config.listen_port,
|
||||
@ -116,14 +105,31 @@ def setup(hass, yaml_config):
|
||||
async def stop_emulated_hue_bridge(event):
|
||||
"""Stop the emulated hue bridge."""
|
||||
upnp_listener.stop()
|
||||
await server.stop()
|
||||
if server:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
await app.shutdown()
|
||||
if handler:
|
||||
await handler.shutdown(10)
|
||||
await app.cleanup()
|
||||
|
||||
async def start_emulated_hue_bridge(event):
|
||||
"""Start the emulated hue bridge."""
|
||||
upnp_listener.start()
|
||||
await server.start()
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)
|
||||
nonlocal handler
|
||||
nonlocal server
|
||||
|
||||
handler = app.make_handler(loop=hass.loop)
|
||||
|
||||
try:
|
||||
server = await hass.loop.create_server(
|
||||
handler, config.host_ip_addr, config.listen_port)
|
||||
except OSError as error:
|
||||
_LOGGER.error("Failed to create HTTP server at port %d: %s",
|
||||
config.listen_port, error)
|
||||
else:
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
|
||||
|
||||
|
@ -75,6 +75,7 @@ class EnOceanDongle:
|
||||
_LOGGER.debug("Received radio packet: %s", temp)
|
||||
rxtype = None
|
||||
value = None
|
||||
channel = 0
|
||||
if temp.data[6] == 0x30:
|
||||
rxtype = "wallswitch"
|
||||
value = 1
|
||||
@ -84,8 +85,9 @@ class EnOceanDongle:
|
||||
elif temp.data[4] == 0x0c:
|
||||
rxtype = "power"
|
||||
value = temp.data[3] + (temp.data[2] << 8)
|
||||
elif temp.data[2] == 0x60:
|
||||
elif temp.data[2] & 0x60 == 0x60:
|
||||
rxtype = "switch_status"
|
||||
channel = temp.data[2] & 0x1F
|
||||
if temp.data[3] == 0xe4:
|
||||
value = 1
|
||||
elif temp.data[3] == 0x80:
|
||||
@ -104,7 +106,8 @@ class EnOceanDongle:
|
||||
if temp.sender_int == self._combine_hex(device.dev_id):
|
||||
if value > 10:
|
||||
device.value_changed(1)
|
||||
if rxtype == "switch_status" and device.stype == "switch":
|
||||
if rxtype == "switch_status" and device.stype == "switch" and \
|
||||
channel == device.channel:
|
||||
if temp.sender_int == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
if rxtype == "dimmerstatus" and device.stype == "dimmer":
|
||||
|
@ -49,7 +49,6 @@ EUFY_DISPATCH = {
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up Eufy devices."""
|
||||
# pylint: disable=import-error
|
||||
import lakeside
|
||||
|
||||
if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]:
|
||||
|
0
homeassistant/components/fritzbox.py
Executable file → Normal file
0
homeassistant/components/fritzbox.py
Executable file → Normal file
@ -26,10 +26,10 @@ from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.yaml import load_yaml
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180708.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180720.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', 'onboarding']
|
||||
|
||||
CONF_THEMES = 'themes'
|
||||
CONF_EXTRA_HTML_URL = 'extra_html_url'
|
||||
@ -50,7 +50,7 @@ MANIFEST_JSON = {
|
||||
'lang': 'en-US',
|
||||
'name': 'Home Assistant',
|
||||
'short_name': 'Assistant',
|
||||
'start_url': '/states',
|
||||
'start_url': '/?homescreen=1',
|
||||
'theme_color': DEFAULT_THEME_COLOR
|
||||
}
|
||||
|
||||
@ -200,15 +200,6 @@ def add_manifest_json_key(key, val):
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the serving of the frontend."""
|
||||
if hass.auth.active:
|
||||
client = await hass.auth.async_get_or_create_client(
|
||||
'Home Assistant Frontend',
|
||||
redirect_uris=['/'],
|
||||
no_secret=True,
|
||||
)
|
||||
else:
|
||||
client = None
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
@ -255,7 +246,7 @@ async def async_setup(hass, config):
|
||||
if os.path.isdir(local):
|
||||
hass.http.register_static_path("/local", local, not is_dev)
|
||||
|
||||
index_view = IndexView(repo_path, js_version, client)
|
||||
index_view = IndexView(repo_path, js_version, hass.auth.active)
|
||||
hass.http.register_view(index_view)
|
||||
|
||||
@callback
|
||||
@ -266,7 +257,7 @@ async def async_setup(hass, config):
|
||||
await asyncio.wait(
|
||||
[async_register_built_in_panel(hass, panel) for panel in (
|
||||
'dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template', 'dev-mqtt', 'kiosk', 'lovelace')],
|
||||
'dev-template', 'dev-mqtt', 'kiosk', 'lovelace', 'profile')],
|
||||
loop=hass.loop)
|
||||
|
||||
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
|
||||
@ -350,11 +341,11 @@ class IndexView(HomeAssistantView):
|
||||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/{extra}']
|
||||
|
||||
def __init__(self, repo_path, js_option, client):
|
||||
def __init__(self, repo_path, js_option, auth_active):
|
||||
"""Initialize the frontend view."""
|
||||
self.repo_path = repo_path
|
||||
self.js_option = js_option
|
||||
self.client = client
|
||||
self.auth_active = auth_active
|
||||
self._template_cache = {}
|
||||
|
||||
def get_template(self, latest):
|
||||
@ -386,11 +377,23 @@ class IndexView(HomeAssistantView):
|
||||
latest = self.repo_path is not None or \
|
||||
_is_latest(self.js_option, request)
|
||||
|
||||
if not hass.components.onboarding.async_is_onboarded():
|
||||
if latest:
|
||||
location = '/frontend_latest/onboarding.html'
|
||||
else:
|
||||
location = '/frontend_es5/onboarding.html'
|
||||
|
||||
return web.Response(status=302, headers={
|
||||
'location': location
|
||||
})
|
||||
|
||||
no_auth = '1'
|
||||
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
|
||||
# do not try to auto connect on load
|
||||
no_auth = '0'
|
||||
|
||||
use_oauth = '1' if self.auth_active else '0'
|
||||
|
||||
template = await hass.async_add_job(self.get_template, latest)
|
||||
|
||||
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
|
||||
@ -399,11 +402,9 @@ class IndexView(HomeAssistantView):
|
||||
no_auth=no_auth,
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[extra_key],
|
||||
use_oauth=use_oauth
|
||||
)
|
||||
|
||||
if self.client is not None:
|
||||
template_params['client_id'] = self.client.id
|
||||
|
||||
return web.Response(text=template.render(**template_params),
|
||||
content_type='text/html')
|
||||
|
||||
@ -489,7 +490,7 @@ def websocket_get_translations(hass, connection, msg):
|
||||
Async friendly.
|
||||
"""
|
||||
async def send_translations():
|
||||
"""Send a camera still."""
|
||||
"""Send a translation."""
|
||||
resources = await async_get_translations(hass, msg['language'])
|
||||
connection.send_message_outside(websocket_api.result_message(
|
||||
msg['id'], {
|
||||
|
@ -25,6 +25,7 @@ from homeassistant.util import convert, dt
|
||||
|
||||
REQUIREMENTS = [
|
||||
'google-api-python-client==1.6.4',
|
||||
'httplib2==0.10.3',
|
||||
'oauth2client==4.0.0',
|
||||
]
|
||||
|
||||
|
@ -10,10 +10,8 @@ from aiohttp.hdrs import AUTHORIZATION
|
||||
from aiohttp.web import Request, Response
|
||||
|
||||
# Typing imports
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant, callback # NOQA
|
||||
from homeassistant.helpers.entity import Entity # NOQA
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import (
|
||||
GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
|
@ -3,14 +3,6 @@ import collections
|
||||
from itertools import product
|
||||
import logging
|
||||
|
||||
# Typing imports
|
||||
# pylint: disable=unused-import
|
||||
# if False:
|
||||
from aiohttp.web import Request, Response # NOQA
|
||||
from typing import Dict, Tuple, Any, Optional # NOQA
|
||||
from homeassistant.helpers.entity import Entity # NOQA
|
||||
from homeassistant.core import HomeAssistant # NOQA
|
||||
from homeassistant.util.unit_system import UnitSystem # NOQA
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.discovery import SERVICE_HOMEKIT
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['homekit==0.6']
|
||||
REQUIREMENTS = ['homekit==0.10']
|
||||
|
||||
DOMAIN = 'homekit_controller'
|
||||
HOMEKIT_DIR = '.homekit'
|
||||
@ -26,6 +26,12 @@ HOMEKIT_ACCESSORY_DISPATCH = {
|
||||
'thermostat': 'climate',
|
||||
}
|
||||
|
||||
HOMEKIT_IGNORE = [
|
||||
'BSB002',
|
||||
'Home Assistant Bridge',
|
||||
'TRADFRI gateway'
|
||||
]
|
||||
|
||||
KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
|
||||
KNOWN_DEVICES = "{}-devices".format(DOMAIN)
|
||||
|
||||
@ -237,6 +243,9 @@ def setup(hass, config):
|
||||
hkid = discovery_info['properties']['id']
|
||||
config_num = int(discovery_info['properties']['c#'])
|
||||
|
||||
if model in HOMEKIT_IGNORE:
|
||||
return
|
||||
|
||||
# Only register a device once, but rescan if the config has changed
|
||||
if hkid in hass.data[KNOWN_DEVICES]:
|
||||
device = hass.data[KNOWN_DEVICES][hkid]
|
||||
|
@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.44']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.45']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -71,7 +71,7 @@ HM_DEVICE_TYPES = {
|
||||
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
|
||||
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
|
||||
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
|
||||
'IPWeatherSensor', 'RotaryHandleSensorIP'],
|
||||
'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor'],
|
||||
DISCOVER_CLIMATE: [
|
||||
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
|
||||
@ -80,7 +80,7 @@ HM_DEVICE_TYPES = {
|
||||
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
|
||||
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
|
||||
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
|
||||
'WiredSensor', 'PresenceIP', 'IPWeatherSensor'],
|
||||
'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor'],
|
||||
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
|
||||
DISCOVER_LOCKS: ['KeyMatic']
|
||||
}
|
||||
@ -114,7 +114,7 @@ HM_ATTRIBUTE_SUPPORT = {
|
||||
'CURRENT': ['current', {}],
|
||||
'VOLTAGE': ['voltage', {}],
|
||||
'OPERATING_VOLTAGE': ['voltage', {}],
|
||||
'WORKING': ['working', {0: 'No', 1: 'Yes'}],
|
||||
'WORKING': ['working', {0: 'No', 1: 'Yes'}]
|
||||
}
|
||||
|
||||
HM_PRESS_EVENTS = [
|
||||
|
@ -1,262 +0,0 @@
|
||||
"""
|
||||
Support for HomematicIP components.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homematicip_cloud/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.core import callback
|
||||
|
||||
REQUIREMENTS = ['homematicip==0.9.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'homematicip_cloud'
|
||||
|
||||
COMPONENTS = [
|
||||
'sensor',
|
||||
'binary_sensor',
|
||||
'switch',
|
||||
'light',
|
||||
'climate',
|
||||
]
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_ACCESSPOINT = 'accesspoint'
|
||||
CONF_AUTHTOKEN = 'authtoken'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Optional(CONF_NAME): vol.Any(cv.string),
|
||||
vol.Required(CONF_ACCESSPOINT): cv.string,
|
||||
vol.Required(CONF_AUTHTOKEN): cv.string,
|
||||
})]),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
HMIP_ACCESS_POINT = 'Access Point'
|
||||
HMIP_HUB = 'HmIP-HUB'
|
||||
|
||||
ATTR_HOME_ID = 'home_id'
|
||||
ATTR_HOME_NAME = 'home_name'
|
||||
ATTR_DEVICE_ID = 'device_id'
|
||||
ATTR_DEVICE_LABEL = 'device_label'
|
||||
ATTR_STATUS_UPDATE = 'status_update'
|
||||
ATTR_FIRMWARE_STATE = 'firmware_state'
|
||||
ATTR_UNREACHABLE = 'unreachable'
|
||||
ATTR_LOW_BATTERY = 'low_battery'
|
||||
ATTR_MODEL_TYPE = 'model_type'
|
||||
ATTR_GROUP_TYPE = 'group_type'
|
||||
ATTR_DEVICE_RSSI = 'device_rssi'
|
||||
ATTR_DUTY_CYCLE = 'duty_cycle'
|
||||
ATTR_CONNECTED = 'connected'
|
||||
ATTR_SABOTAGE = 'sabotage'
|
||||
ATTR_OPERATION_LOCK = 'operation_lock'
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the HomematicIP component."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
accesspoints = config.get(DOMAIN, [])
|
||||
for conf in accesspoints:
|
||||
_websession = async_get_clientsession(hass)
|
||||
_hmip = HomematicipConnector(hass, conf, _websession)
|
||||
try:
|
||||
await _hmip.init()
|
||||
except HmipConnectionError:
|
||||
_LOGGER.error('Failed to connect to the HomematicIP server, %s.',
|
||||
conf.get(CONF_ACCESSPOINT))
|
||||
return False
|
||||
|
||||
home = _hmip.home
|
||||
home.name = conf.get(CONF_NAME)
|
||||
home.label = HMIP_ACCESS_POINT
|
||||
home.modelType = HMIP_HUB
|
||||
|
||||
hass.data[DOMAIN][home.id] = home
|
||||
_LOGGER.info('Connected to the HomematicIP server, %s.',
|
||||
conf.get(CONF_ACCESSPOINT))
|
||||
homeid = {ATTR_HOME_ID: home.id}
|
||||
for component in COMPONENTS:
|
||||
hass.async_add_job(async_load_platform(hass, component, DOMAIN,
|
||||
homeid, config))
|
||||
|
||||
hass.loop.create_task(_hmip.connect())
|
||||
return True
|
||||
|
||||
|
||||
class HomematicipConnector:
|
||||
"""Manages HomematicIP http and websocket connection."""
|
||||
|
||||
def __init__(self, hass, config, websession):
|
||||
"""Initialize HomematicIP cloud connection."""
|
||||
from homematicip.async.home import AsyncHome
|
||||
|
||||
self._hass = hass
|
||||
self._ws_close_requested = False
|
||||
self._retry_task = None
|
||||
self._tries = 0
|
||||
self._accesspoint = config.get(CONF_ACCESSPOINT)
|
||||
_authtoken = config.get(CONF_AUTHTOKEN)
|
||||
|
||||
self.home = AsyncHome(hass.loop, websession)
|
||||
self.home.set_auth_token(_authtoken)
|
||||
|
||||
self.home.on_update(self.async_update)
|
||||
self._accesspoint_connected = True
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close())
|
||||
|
||||
async def init(self):
|
||||
"""Initialize connection."""
|
||||
await self.home.init(self._accesspoint)
|
||||
await self.home.get_current_state()
|
||||
|
||||
@callback
|
||||
def async_update(self, *args, **kwargs):
|
||||
"""Async update the home device.
|
||||
|
||||
Triggered when the hmip HOME_CHANGED event has fired.
|
||||
There are several occasions for this event to happen.
|
||||
We are only interested to check whether the access point
|
||||
is still connected. If not, device state changes cannot
|
||||
be forwarded to hass. So if access point is disconnected all devices
|
||||
are set to unavailable.
|
||||
"""
|
||||
if not self.home.connected:
|
||||
_LOGGER.error(
|
||||
"HMIP access point has lost connection with the cloud")
|
||||
self._accesspoint_connected = False
|
||||
self.set_all_to_unavailable()
|
||||
elif not self._accesspoint_connected:
|
||||
# Explicitly getting an update as device states might have
|
||||
# changed during access point disconnect."""
|
||||
|
||||
job = self._hass.async_add_job(self.get_state())
|
||||
job.add_done_callback(self.get_state_finished)
|
||||
|
||||
async def get_state(self):
|
||||
"""Update hmip state and tell hass."""
|
||||
await self.home.get_current_state()
|
||||
self.update_all()
|
||||
|
||||
def get_state_finished(self, future):
|
||||
"""Execute when get_state coroutine has finished."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
try:
|
||||
future.result()
|
||||
except HmipConnectionError:
|
||||
# Somehow connection could not recover. Will disconnect and
|
||||
# so reconnect loop is taking over.
|
||||
_LOGGER.error(
|
||||
"updating state after himp access point reconnect failed.")
|
||||
self._hass.async_add_job(self.home.disable_events())
|
||||
|
||||
def set_all_to_unavailable(self):
|
||||
"""Set all devices to unavailable and tell Hass."""
|
||||
for device in self.home.devices:
|
||||
device.unreach = True
|
||||
self.update_all()
|
||||
|
||||
def update_all(self):
|
||||
"""Signal all devices to update their state."""
|
||||
for device in self.home.devices:
|
||||
device.fire_update_event()
|
||||
|
||||
async def _handle_connection(self):
|
||||
"""Handle websocket connection."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
await self.home.get_current_state()
|
||||
hmip_events = await self.home.enable_events()
|
||||
try:
|
||||
await hmip_events
|
||||
except HmipConnectionError:
|
||||
return
|
||||
|
||||
async def connect(self):
|
||||
"""Start websocket connection."""
|
||||
self._tries = 0
|
||||
while True:
|
||||
await self._handle_connection()
|
||||
if self._ws_close_requested:
|
||||
break
|
||||
self._ws_close_requested = False
|
||||
self._tries += 1
|
||||
try:
|
||||
self._retry_task = self._hass.async_add_job(asyncio.sleep(
|
||||
2 ** min(9, self._tries), loop=self._hass.loop))
|
||||
await self._retry_task
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
_LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.',
|
||||
self._tries)
|
||||
|
||||
async def close(self):
|
||||
"""Close the websocket connection."""
|
||||
self._ws_close_requested = True
|
||||
if self._retry_task is not None:
|
||||
self._retry_task.cancel()
|
||||
await self.home.disable_events()
|
||||
_LOGGER.info("Closed connection to HomematicIP cloud server.")
|
||||
|
||||
|
||||
class HomematicipGenericDevice(Entity):
|
||||
"""Representation of an HomematicIP generic device."""
|
||||
|
||||
def __init__(self, home, device, post=None):
|
||||
"""Initialize the generic device."""
|
||||
self._home = home
|
||||
self._device = device
|
||||
self.post = post
|
||||
_LOGGER.info('Setting up %s (%s)', self.name,
|
||||
self._device.modelType)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._device.on_update(self._device_changed)
|
||||
|
||||
def _device_changed(self, json, **kwargs):
|
||||
"""Handle device state changes."""
|
||||
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the generic device."""
|
||||
name = self._device.label
|
||||
if self._home.name is not None:
|
||||
name = "{} {}".format(self._home.name, name)
|
||||
if self.post is not None:
|
||||
name = "{} {}".format(name, self.post)
|
||||
return name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Device available."""
|
||||
return not self._device.unreach
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the generic device."""
|
||||
return {
|
||||
ATTR_LOW_BATTERY: self._device.lowBat,
|
||||
ATTR_MODEL_TYPE: self._device.modelType
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "HomematicIP Cloud",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Pick HomematicIP Accesspoint",
|
||||
"data": {
|
||||
"hapid": "Accesspoint ID (SGTIN)",
|
||||
"pin": "Pin Code (optional)",
|
||||
"name": "Name (optional, used as name prefix for all devices)"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link Accesspoint",
|
||||
"description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"register_failed": "Failed to register, please try again.",
|
||||
"invalid_pin": "Invalid PIN, please try again.",
|
||||
"press_the_button": "Please press the blue button.",
|
||||
"timeout_button": "Blue button press timeout, please try again."
|
||||
},
|
||||
"abort": {
|
||||
"unknown": "Unknown error occurred.",
|
||||
"conection_aborted": "Could not connect to HMIP server",
|
||||
"already_configured": "Accesspoint is already configured"
|
||||
}
|
||||
}
|
||||
}
|
65
homeassistant/components/homematicip_cloud/__init__.py
Normal file
65
homeassistant/components/homematicip_cloud/__init__.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""
|
||||
Support for HomematicIP components.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import (
|
||||
DOMAIN, HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME,
|
||||
CONF_ACCESSPOINT, CONF_AUTHTOKEN, CONF_NAME)
|
||||
# Loading the config flow file will register the flow
|
||||
from .config_flow import configured_haps
|
||||
from .hap import HomematicipHAP, HomematicipAuth # noqa: F401
|
||||
from .device import HomematicipGenericDevice # noqa: F401
|
||||
|
||||
REQUIREMENTS = ['homematicip==0.9.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=''): vol.Any(cv.string),
|
||||
vol.Required(CONF_ACCESSPOINT): cv.string,
|
||||
vol.Required(CONF_AUTHTOKEN): cv.string,
|
||||
})]),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the HomematicIP component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
accesspoints = config.get(DOMAIN, [])
|
||||
|
||||
for conf in accesspoints:
|
||||
if conf[CONF_ACCESSPOINT] not in configured_haps(hass):
|
||||
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||
DOMAIN, source='import', data={
|
||||
HMIPC_HAPID: conf[CONF_ACCESSPOINT],
|
||||
HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN],
|
||||
HMIPC_NAME: conf[CONF_NAME],
|
||||
}
|
||||
))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up an accsspoint from a config entry."""
|
||||
hap = HomematicipHAP(hass, entry)
|
||||
hapid = entry.data[HMIPC_HAPID].replace('-', '').upper()
|
||||
hass.data[DOMAIN][hapid] = hap
|
||||
return await hap.async_setup()
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID])
|
||||
return await hap.async_reset()
|
97
homeassistant/components/homematicip_cloud/config_flow.py
Normal file
97
homeassistant/components/homematicip_cloud/config_flow.py
Normal file
@ -0,0 +1,97 @@
|
||||
"""Config flow to configure HomematicIP Cloud."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import (
|
||||
DOMAIN as HMIPC_DOMAIN, _LOGGER,
|
||||
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME)
|
||||
from .hap import HomematicipAuth
|
||||
|
||||
|
||||
@callback
|
||||
def configured_haps(hass):
|
||||
"""Return a set of the configured accesspoints."""
|
||||
return set(entry.data[HMIPC_HAPID] for entry
|
||||
in hass.config_entries.async_entries(HMIPC_DOMAIN))
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(HMIPC_DOMAIN)
|
||||
class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
|
||||
"""Config flow HomematicIP Cloud."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize HomematicIP Cloud config flow."""
|
||||
self.auth = None
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
user_input[HMIPC_HAPID] = \
|
||||
user_input[HMIPC_HAPID].replace('-', '').upper()
|
||||
if user_input[HMIPC_HAPID] in configured_haps(self.hass):
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
self.auth = HomematicipAuth(self.hass, user_input)
|
||||
connected = await self.auth.async_setup()
|
||||
if connected:
|
||||
_LOGGER.info("Connection established")
|
||||
return await self.async_step_link()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(HMIPC_HAPID): str,
|
||||
vol.Optional(HMIPC_PIN): str,
|
||||
vol.Optional(HMIPC_NAME): str,
|
||||
}),
|
||||
errors=errors
|
||||
)
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with the HomematicIP Cloud accesspoint."""
|
||||
errors = {}
|
||||
|
||||
pressed = await self.auth.async_checkbutton()
|
||||
if pressed:
|
||||
authtoken = await self.auth.async_register()
|
||||
if authtoken:
|
||||
_LOGGER.info("Write config entry")
|
||||
return self.async_create_entry(
|
||||
title=self.auth.config.get(HMIPC_HAPID),
|
||||
data={
|
||||
HMIPC_HAPID: self.auth.config.get(HMIPC_HAPID),
|
||||
HMIPC_AUTHTOKEN: authtoken,
|
||||
HMIPC_NAME: self.auth.config.get(HMIPC_NAME)
|
||||
})
|
||||
return self.async_abort(reason='conection_aborted')
|
||||
else:
|
||||
errors['base'] = 'press_the_button'
|
||||
|
||||
return self.async_show_form(step_id='link', errors=errors)
|
||||
|
||||
async def async_step_import(self, import_info):
|
||||
"""Import a new bridge as a config entry."""
|
||||
hapid = import_info[HMIPC_HAPID]
|
||||
authtoken = import_info[HMIPC_AUTHTOKEN]
|
||||
name = import_info[HMIPC_NAME]
|
||||
|
||||
hapid = hapid.replace('-', '').upper()
|
||||
if hapid in configured_haps(self.hass):
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
_LOGGER.info('Imported authentication for %s', hapid)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=hapid,
|
||||
data={
|
||||
HMIPC_HAPID: hapid,
|
||||
HMIPC_AUTHTOKEN: authtoken,
|
||||
HMIPC_NAME: name
|
||||
}
|
||||
)
|
24
homeassistant/components/homematicip_cloud/const.py
Normal file
24
homeassistant/components/homematicip_cloud/const.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""Constants for the HomematicIP Cloud component."""
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger('homeassistant.components.homematicip_cloud')
|
||||
|
||||
DOMAIN = 'homematicip_cloud'
|
||||
|
||||
COMPONENTS = [
|
||||
'alarm_control_panel',
|
||||
'binary_sensor',
|
||||
'climate',
|
||||
'light',
|
||||
'sensor',
|
||||
'switch',
|
||||
]
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_ACCESSPOINT = 'accesspoint'
|
||||
CONF_AUTHTOKEN = 'authtoken'
|
||||
|
||||
HMIPC_NAME = 'name'
|
||||
HMIPC_HAPID = 'hapid'
|
||||
HMIPC_AUTHTOKEN = 'authtoken'
|
||||
HMIPC_PIN = 'pin'
|
71
homeassistant/components/homematicip_cloud/device.py
Normal file
71
homeassistant/components/homematicip_cloud/device.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""GenericDevice for the HomematicIP Cloud component."""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_HOME_ID = 'home_id'
|
||||
ATTR_HOME_NAME = 'home_name'
|
||||
ATTR_DEVICE_ID = 'device_id'
|
||||
ATTR_DEVICE_LABEL = 'device_label'
|
||||
ATTR_STATUS_UPDATE = 'status_update'
|
||||
ATTR_FIRMWARE_STATE = 'firmware_state'
|
||||
ATTR_UNREACHABLE = 'unreachable'
|
||||
ATTR_LOW_BATTERY = 'low_battery'
|
||||
ATTR_MODEL_TYPE = 'model_type'
|
||||
ATTR_GROUP_TYPE = 'group_type'
|
||||
ATTR_DEVICE_RSSI = 'device_rssi'
|
||||
ATTR_DUTY_CYCLE = 'duty_cycle'
|
||||
ATTR_CONNECTED = 'connected'
|
||||
ATTR_SABOTAGE = 'sabotage'
|
||||
ATTR_OPERATION_LOCK = 'operation_lock'
|
||||
|
||||
|
||||
class HomematicipGenericDevice(Entity):
|
||||
"""Representation of an HomematicIP generic device."""
|
||||
|
||||
def __init__(self, home, device, post=None):
|
||||
"""Initialize the generic device."""
|
||||
self._home = home
|
||||
self._device = device
|
||||
self.post = post
|
||||
_LOGGER.info('Setting up %s (%s)', self.name,
|
||||
self._device.modelType)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._device.on_update(self._device_changed)
|
||||
|
||||
def _device_changed(self, json, **kwargs):
|
||||
"""Handle device state changes."""
|
||||
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the generic device."""
|
||||
name = self._device.label
|
||||
if (self._home.name is not None and self._home.name != ''):
|
||||
name = "{} {}".format(self._home.name, name)
|
||||
if (self.post is not None and self.post != ''):
|
||||
name = "{} {}".format(name, self.post)
|
||||
return name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Device available."""
|
||||
return not self._device.unreach
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the generic device."""
|
||||
return {
|
||||
ATTR_LOW_BATTERY: self._device.lowBat,
|
||||
ATTR_MODEL_TYPE: self._device.modelType
|
||||
}
|
22
homeassistant/components/homematicip_cloud/errors.py
Normal file
22
homeassistant/components/homematicip_cloud/errors.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Errors for the HomematicIP component."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
class HmipcException(HomeAssistantError):
|
||||
"""Base class for HomematicIP exceptions."""
|
||||
|
||||
|
||||
class HmipcConnectionError(HmipcException):
|
||||
"""Unable to connect to the HomematicIP cloud server."""
|
||||
|
||||
|
||||
class HmipcConnectionWait(HmipcException):
|
||||
"""Wait for registration to the HomematicIP cloud server."""
|
||||
|
||||
|
||||
class HmipcRegistrationFailed(HmipcException):
|
||||
"""Registration on HomematicIP cloud failed."""
|
||||
|
||||
|
||||
class HmipcPressButton(HmipcException):
|
||||
"""User needs to press the blue button."""
|
256
homeassistant/components/homematicip_cloud/hap.py
Normal file
256
homeassistant/components/homematicip_cloud/hap.py
Normal file
@ -0,0 +1,256 @@
|
||||
"""Accesspoint for the HomematicIP Cloud component."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import (
|
||||
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME,
|
||||
COMPONENTS)
|
||||
from .errors import HmipcConnectionError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HomematicipAuth(object):
|
||||
"""Manages HomematicIP client registration."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize HomematicIP Cloud client registration."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.auth = None
|
||||
|
||||
async def async_setup(self):
|
||||
"""Connect to HomematicIP for registration."""
|
||||
try:
|
||||
self.auth = await self.get_auth(
|
||||
self.hass,
|
||||
self.config.get(HMIPC_HAPID),
|
||||
self.config.get(HMIPC_PIN)
|
||||
)
|
||||
return True
|
||||
except HmipcConnectionError:
|
||||
return False
|
||||
|
||||
async def async_checkbutton(self):
|
||||
"""Check blue butten has been pressed."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
try:
|
||||
await self.auth.isRequestAcknowledged()
|
||||
return True
|
||||
except HmipConnectionError:
|
||||
return False
|
||||
|
||||
async def async_register(self):
|
||||
"""Register client at HomematicIP."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
try:
|
||||
authtoken = await self.auth.requestAuthToken()
|
||||
await self.auth.confirmAuthToken(authtoken)
|
||||
return authtoken
|
||||
except HmipConnectionError:
|
||||
return False
|
||||
|
||||
async def get_auth(self, hass, hapid, pin):
|
||||
"""Create a HomematicIP access point object."""
|
||||
from homematicip.aio.auth import AsyncAuth
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
auth = AsyncAuth(hass.loop, async_get_clientsession(hass))
|
||||
print(auth)
|
||||
try:
|
||||
await auth.init(hapid)
|
||||
if pin:
|
||||
auth.pin = pin
|
||||
await auth.connectionRequest('HomeAssistant')
|
||||
except HmipConnectionError:
|
||||
return False
|
||||
return auth
|
||||
|
||||
|
||||
class HomematicipHAP(object):
|
||||
"""Manages HomematicIP http and websocket connection."""
|
||||
|
||||
def __init__(self, hass, config_entry):
|
||||
"""Initialize HomematicIP cloud connection."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.home = None
|
||||
|
||||
self._ws_close_requested = False
|
||||
self._retry_task = None
|
||||
self._tries = 0
|
||||
self._accesspoint_connected = True
|
||||
self._retry_setup = None
|
||||
|
||||
async def async_setup(self, tries=0):
|
||||
"""Initialize connection."""
|
||||
try:
|
||||
self.home = await self.get_hap(
|
||||
self.hass,
|
||||
self.config_entry.data.get(HMIPC_HAPID),
|
||||
self.config_entry.data.get(HMIPC_AUTHTOKEN),
|
||||
self.config_entry.data.get(HMIPC_NAME)
|
||||
)
|
||||
except HmipcConnectionError:
|
||||
retry_delay = 2 ** min(tries + 1, 6)
|
||||
_LOGGER.error("Error connecting to HomematicIP with HAP %s. "
|
||||
"Retrying in %d seconds.",
|
||||
self.config_entry.data.get(HMIPC_HAPID), retry_delay)
|
||||
|
||||
async def retry_setup(_now):
|
||||
"""Retry setup."""
|
||||
if await self.async_setup(tries + 1):
|
||||
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
|
||||
|
||||
self._retry_setup = self.hass.helpers.event.async_call_later(
|
||||
retry_delay, retry_setup)
|
||||
|
||||
return False
|
||||
|
||||
_LOGGER.info('Connected to HomematicIP with HAP %s.',
|
||||
self.config_entry.data.get(HMIPC_HAPID))
|
||||
|
||||
for component in COMPONENTS:
|
||||
self.hass.async_add_job(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, component)
|
||||
)
|
||||
return True
|
||||
|
||||
@callback
|
||||
def async_update(self, *args, **kwargs):
|
||||
"""Async update the home device.
|
||||
|
||||
Triggered when the hmip HOME_CHANGED event has fired.
|
||||
There are several occasions for this event to happen.
|
||||
We are only interested to check whether the access point
|
||||
is still connected. If not, device state changes cannot
|
||||
be forwarded to hass. So if access point is disconnected all devices
|
||||
are set to unavailable.
|
||||
"""
|
||||
if not self.home.connected:
|
||||
_LOGGER.error(
|
||||
"HMIP access point has lost connection with the cloud")
|
||||
self._accesspoint_connected = False
|
||||
self.set_all_to_unavailable()
|
||||
elif not self._accesspoint_connected:
|
||||
# Explicitly getting an update as device states might have
|
||||
# changed during access point disconnect."""
|
||||
|
||||
job = self.hass.async_add_job(self.get_state())
|
||||
job.add_done_callback(self.get_state_finished)
|
||||
|
||||
async def get_state(self):
|
||||
"""Update hmip state and tell hass."""
|
||||
await self.home.get_current_state()
|
||||
self.update_all()
|
||||
|
||||
def get_state_finished(self, future):
|
||||
"""Execute when get_state coroutine has finished."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
try:
|
||||
future.result()
|
||||
except HmipConnectionError:
|
||||
# Somehow connection could not recover. Will disconnect and
|
||||
# so reconnect loop is taking over.
|
||||
_LOGGER.error(
|
||||
"updating state after himp access point reconnect failed.")
|
||||
self.hass.async_add_job(self.home.disable_events())
|
||||
|
||||
def set_all_to_unavailable(self):
|
||||
"""Set all devices to unavailable and tell Hass."""
|
||||
for device in self.home.devices:
|
||||
device.unreach = True
|
||||
self.update_all()
|
||||
|
||||
def update_all(self):
|
||||
"""Signal all devices to update their state."""
|
||||
for device in self.home.devices:
|
||||
device.fire_update_event()
|
||||
|
||||
async def _handle_connection(self):
|
||||
"""Handle websocket connection."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
try:
|
||||
await self.home.get_current_state()
|
||||
except HmipConnectionError:
|
||||
return
|
||||
hmip_events = await self.home.enable_events()
|
||||
try:
|
||||
await hmip_events
|
||||
except HmipConnectionError:
|
||||
return
|
||||
|
||||
async def async_connect(self):
|
||||
"""Start websocket connection."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
tries = 0
|
||||
while True:
|
||||
try:
|
||||
await self.home.get_current_state()
|
||||
hmip_events = await self.home.enable_events()
|
||||
tries = 0
|
||||
await hmip_events
|
||||
except HmipConnectionError:
|
||||
pass
|
||||
|
||||
if self._ws_close_requested:
|
||||
break
|
||||
self._ws_close_requested = False
|
||||
|
||||
tries += 1
|
||||
retry_delay = 2 ** min(tries + 1, 6)
|
||||
_LOGGER.error("Error connecting to HomematicIP with HAP %s. "
|
||||
"Retrying in %d seconds.",
|
||||
self.config_entry.data.get(HMIPC_HAPID), retry_delay)
|
||||
try:
|
||||
self._retry_task = self.hass.async_add_job(asyncio.sleep(
|
||||
retry_delay, loop=self.hass.loop))
|
||||
await self._retry_task
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
async def async_reset(self):
|
||||
"""Close the websocket connection."""
|
||||
self._ws_close_requested = True
|
||||
if self._retry_setup is not None:
|
||||
self._retry_setup.cancel()
|
||||
if self._retry_task is not None:
|
||||
self._retry_task.cancel()
|
||||
self.home.disable_events()
|
||||
_LOGGER.info("Closed connection to HomematicIP cloud server.")
|
||||
for component in COMPONENTS:
|
||||
await self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, component)
|
||||
return True
|
||||
|
||||
async def get_hap(self, hass, hapid, authtoken, name):
|
||||
"""Create a HomematicIP access point object."""
|
||||
from homematicip.aio.home import AsyncHome
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
home = AsyncHome(hass.loop, async_get_clientsession(hass))
|
||||
|
||||
home.name = name
|
||||
home.label = 'Access Point'
|
||||
home.modelType = 'HmIP-HAP'
|
||||
|
||||
home.set_auth_token(authtoken)
|
||||
try:
|
||||
await home.init(hapid)
|
||||
await home.get_current_state()
|
||||
except HmipConnectionError:
|
||||
raise HmipcConnectionError
|
||||
home.on_update(self.async_update)
|
||||
hass.loop.create_task(self.async_connect())
|
||||
|
||||
return home
|
30
homeassistant/components/homematicip_cloud/strings.json
Normal file
30
homeassistant/components/homematicip_cloud/strings.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "HomematicIP Cloud",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Pick HomematicIP Accesspoint",
|
||||
"data": {
|
||||
"hapid": "Accesspoint ID (SGTIN)",
|
||||
"pin": "Pin Code (optional)",
|
||||
"name": "Name (optional, used as name prefix for all devices)"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link Accesspoint",
|
||||
"description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"register_failed": "Failed to register, please try again.",
|
||||
"invalid_pin": "Invalid PIN, please try again.",
|
||||
"press_the_button": "Please press the blue button.",
|
||||
"timeout_button": "Blue button press timeout, please try again."
|
||||
},
|
||||
"abort": {
|
||||
"unknown": "Unknown error occurred.",
|
||||
"conection_aborted": "Could not connect to HMIP server",
|
||||
"already_configured": "Accesspoint is already configured"
|
||||
}
|
||||
}
|
||||
}
|
@ -187,8 +187,7 @@ class HomeAssistantHTTP(object):
|
||||
support_legacy=hass.auth.support_legacy,
|
||||
api_password=api_password)
|
||||
|
||||
if cors_origins:
|
||||
setup_cors(app, cors_origins)
|
||||
setup_cors(app, cors_origins)
|
||||
|
||||
app['hass'] = hass
|
||||
|
||||
@ -226,7 +225,7 @@ class HomeAssistantHTTP(object):
|
||||
'{0} missing required attribute "name"'.format(class_name)
|
||||
)
|
||||
|
||||
view.register(self.app.router)
|
||||
view.register(self.app, self.app.router)
|
||||
|
||||
def register_redirect(self, url, redirect_to):
|
||||
"""Register a redirect with the server.
|
||||
|
@ -27,7 +27,8 @@ def setup_auth(app, trusted_networks, use_auth,
|
||||
|
||||
if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or
|
||||
DATA_API_PASSWORD in request.query):
|
||||
_LOGGER.warning('Please use access_token instead api_password.')
|
||||
_LOGGER.warning('Please change to use bearer token access %s',
|
||||
request.path)
|
||||
|
||||
legacy_auth = (not use_auth or support_legacy) and api_password
|
||||
if (hdrs.AUTHORIZATION in request.headers and
|
||||
|
@ -72,7 +72,11 @@ async def ban_middleware(request, handler):
|
||||
|
||||
|
||||
async def process_wrong_login(request):
|
||||
"""Process a wrong login attempt."""
|
||||
"""Process a wrong login attempt.
|
||||
|
||||
Increase failed login attempts counter for remote IP address.
|
||||
Add ip ban entry if failed login attempts exceeds threshold.
|
||||
"""
|
||||
remote_addr = request[KEY_REAL_IP]
|
||||
|
||||
msg = ('Login attempt or request with invalid authentication '
|
||||
@ -107,7 +111,28 @@ async def process_wrong_login(request):
|
||||
'Banning IP address', NOTIFICATION_ID_BAN)
|
||||
|
||||
|
||||
class IpBan(object):
|
||||
async def process_success_login(request):
|
||||
"""Process a success login attempt.
|
||||
|
||||
Reset failed login attempts counter for remote IP address.
|
||||
No release IP address from banned list function, it can only be done by
|
||||
manual modify ip bans config file.
|
||||
"""
|
||||
remote_addr = request[KEY_REAL_IP]
|
||||
|
||||
# Check if ban middleware is loaded
|
||||
if (KEY_BANNED_IPS not in request.app or
|
||||
request.app[KEY_LOGIN_THRESHOLD] < 1):
|
||||
return
|
||||
|
||||
if remote_addr in request.app[KEY_FAILED_LOGIN_ATTEMPTS] and \
|
||||
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > 0:
|
||||
_LOGGER.debug('Login success, reset failed login attempts counter'
|
||||
' from %s', remote_addr)
|
||||
request.app[KEY_FAILED_LOGIN_ATTEMPTS].pop(remote_addr)
|
||||
|
||||
|
||||
class IpBan:
|
||||
"""Represents banned IP address."""
|
||||
|
||||
def __init__(self, ip_ban: str, banned_at: datetime = None) -> None:
|
||||
|
@ -27,6 +27,20 @@ def setup_cors(app, origins):
|
||||
) for host in origins
|
||||
})
|
||||
|
||||
def allow_cors(route, methods):
|
||||
"""Allow cors on a route."""
|
||||
cors.add(route, {
|
||||
'*': aiohttp_cors.ResourceOptions(
|
||||
allow_headers=ALLOWED_CORS_HEADERS,
|
||||
allow_methods=methods,
|
||||
)
|
||||
})
|
||||
|
||||
app['allow_cors'] = allow_cors
|
||||
|
||||
if not origins:
|
||||
return
|
||||
|
||||
async def cors_startup(app):
|
||||
"""Initialize cors when app starts up."""
|
||||
cors_added = set()
|
||||
|
@ -12,6 +12,7 @@ from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError
|
||||
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.components.http.ban import process_success_login
|
||||
from homeassistant.core import is_callback
|
||||
from homeassistant.const import CONTENT_TYPE_JSON
|
||||
|
||||
@ -26,7 +27,9 @@ class HomeAssistantView(object):
|
||||
|
||||
url = None
|
||||
extra_urls = []
|
||||
requires_auth = True # Views inheriting from this class can override this
|
||||
# Views inheriting from this class can override this
|
||||
requires_auth = True
|
||||
cors_allowed = False
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def json(self, result, status_code=200, headers=None):
|
||||
@ -51,10 +54,11 @@ class HomeAssistantView(object):
|
||||
data['code'] = message_code
|
||||
return self.json(data, status_code, headers=headers)
|
||||
|
||||
def register(self, router):
|
||||
def register(self, app, router):
|
||||
"""Register the view with a router."""
|
||||
assert self.url is not None, 'No url set for view'
|
||||
urls = [self.url] + self.extra_urls
|
||||
routes = []
|
||||
|
||||
for method in ('get', 'post', 'delete', 'put'):
|
||||
handler = getattr(self, method, None)
|
||||
@ -65,13 +69,15 @@ class HomeAssistantView(object):
|
||||
handler = request_handler_factory(self, handler)
|
||||
|
||||
for url in urls:
|
||||
router.add_route(method, url, handler)
|
||||
routes.append(
|
||||
(method, router.add_route(method, url, handler))
|
||||
)
|
||||
|
||||
# aiohttp_cors does not work with class based views
|
||||
# self.app.router.add_route('*', self.url, self, name=self.name)
|
||||
if not self.cors_allowed:
|
||||
return
|
||||
|
||||
# for url in self.extra_urls:
|
||||
# self.app.router.add_route('*', url, self)
|
||||
for method, route in routes:
|
||||
app['allow_cors'](route, [method.upper()])
|
||||
|
||||
|
||||
def request_handler_factory(view, handler):
|
||||
@ -86,8 +92,11 @@ def request_handler_factory(view, handler):
|
||||
|
||||
authenticated = request.get(KEY_AUTHENTICATED, False)
|
||||
|
||||
if view.requires_auth and not authenticated:
|
||||
raise HTTPUnauthorized()
|
||||
if view.requires_auth:
|
||||
if authenticated:
|
||||
await process_success_login(request)
|
||||
else:
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
_LOGGER.info('Serving %s to %s (auth: %s)',
|
||||
request.path, request.get(KEY_REAL_IP), authenticated)
|
||||
|
@ -69,27 +69,32 @@ SERVICE_SCAN_SCHEMA = vol.Schema({
|
||||
|
||||
@bind_hass
|
||||
def scan(hass, entity_id=None):
|
||||
"""Force process an image."""
|
||||
"""Force process of all cameras or given entity."""
|
||||
hass.add_job(async_scan, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_scan(hass, entity_id=None):
|
||||
"""Force process of all cameras or given entity."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_SCAN, data)
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the image processing."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_scan_service(service):
|
||||
async def async_scan_service(service):
|
||||
"""Service handler for scan."""
|
||||
image_entities = component.async_extract_from_service(service)
|
||||
|
||||
update_task = [entity.async_update_ha_state(True) for
|
||||
entity in image_entities]
|
||||
if update_task:
|
||||
yield from asyncio.wait(update_task, loop=hass.loop)
|
||||
await asyncio.wait(update_task, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_scan_service,
|
||||
@ -124,8 +129,7 @@ class ImageProcessingEntity(Entity):
|
||||
"""
|
||||
return self.hass.async_add_job(self.process_image, image)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Update image and process it.
|
||||
|
||||
This method is a coroutine.
|
||||
@ -134,7 +138,7 @@ class ImageProcessingEntity(Entity):
|
||||
image = None
|
||||
|
||||
try:
|
||||
image = yield from camera.async_get_image(
|
||||
image = await camera.async_get_image(
|
||||
self.camera_entity, timeout=self.timeout)
|
||||
|
||||
except HomeAssistantError as err:
|
||||
@ -142,7 +146,7 @@ class ImageProcessingEntity(Entity):
|
||||
return
|
||||
|
||||
# process image data
|
||||
yield from self.async_process_image(image.content)
|
||||
await self.async_process_image(image.content)
|
||||
|
||||
|
||||
class ImageProcessingFaceEntity(ImageProcessingEntity):
|
||||
|
@ -10,20 +10,26 @@ import logging
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_NAME)
|
||||
from homeassistant.core import split_entity_id
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.image_processing import (
|
||||
PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE,
|
||||
CONF_ENTITY_ID, CONF_NAME)
|
||||
CONF_ENTITY_ID, CONF_NAME, DOMAIN)
|
||||
from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_BOUNDING_BOX = 'bounding_box'
|
||||
ATTR_CLASSIFIER = 'classifier'
|
||||
ATTR_IMAGE_ID = 'image_id'
|
||||
ATTR_MATCHED = 'matched'
|
||||
CLASSIFIER = 'facebox'
|
||||
DATA_FACEBOX = 'facebox_classifiers'
|
||||
EVENT_CLASSIFIER_TEACH = 'image_processing.teach_classifier'
|
||||
FILE_PATH = 'file_path'
|
||||
SERVICE_TEACH_FACE = 'facebox_teach_face'
|
||||
TIMEOUT = 9
|
||||
|
||||
|
||||
@ -32,6 +38,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
})
|
||||
|
||||
SERVICE_TEACH_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(FILE_PATH): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def encode_image(image):
|
||||
"""base64 encode an image stream."""
|
||||
@ -63,18 +75,65 @@ def parse_faces(api_faces):
|
||||
return known_faces
|
||||
|
||||
|
||||
def post_image(url, image):
|
||||
"""Post an image to the classifier."""
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json={"base64": encode_image(image)},
|
||||
timeout=TIMEOUT
|
||||
)
|
||||
return response
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
|
||||
|
||||
|
||||
def valid_file_path(file_path):
|
||||
"""Check that a file_path points to a valid file."""
|
||||
try:
|
||||
cv.isfile(file_path)
|
||||
return True
|
||||
except vol.Invalid:
|
||||
_LOGGER.error(
|
||||
"%s error: Invalid file path: %s", CLASSIFIER, file_path)
|
||||
return False
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the classifier."""
|
||||
if DATA_FACEBOX not in hass.data:
|
||||
hass.data[DATA_FACEBOX] = []
|
||||
|
||||
entities = []
|
||||
for camera in config[CONF_SOURCE]:
|
||||
entities.append(FaceClassifyEntity(
|
||||
facebox = FaceClassifyEntity(
|
||||
config[CONF_IP_ADDRESS],
|
||||
config[CONF_PORT],
|
||||
camera[CONF_ENTITY_ID],
|
||||
camera.get(CONF_NAME)
|
||||
))
|
||||
camera.get(CONF_NAME))
|
||||
entities.append(facebox)
|
||||
hass.data[DATA_FACEBOX].append(facebox)
|
||||
add_devices(entities)
|
||||
|
||||
def service_handle(service):
|
||||
"""Handle for services."""
|
||||
entity_ids = service.data.get('entity_id')
|
||||
|
||||
classifiers = hass.data[DATA_FACEBOX]
|
||||
if entity_ids:
|
||||
classifiers = [c for c in classifiers if c.entity_id in entity_ids]
|
||||
|
||||
for classifier in classifiers:
|
||||
name = service.data.get(ATTR_NAME)
|
||||
file_path = service.data.get(FILE_PATH)
|
||||
classifier.teach(name, file_path)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
SERVICE_TEACH_FACE,
|
||||
service_handle,
|
||||
schema=SERVICE_TEACH_SCHEMA)
|
||||
|
||||
|
||||
class FaceClassifyEntity(ImageProcessingFaceEntity):
|
||||
"""Perform a face classification."""
|
||||
@ -82,7 +141,8 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
|
||||
def __init__(self, ip, port, camera_entity, name=None):
|
||||
"""Init with the API key and model id."""
|
||||
super().__init__()
|
||||
self._url = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER)
|
||||
self._url_check = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER)
|
||||
self._url_teach = "http://{}:{}/{}/teach".format(ip, port, CLASSIFIER)
|
||||
self._camera = camera_entity
|
||||
if name:
|
||||
self._name = name
|
||||
@ -94,28 +154,54 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process an image."""
|
||||
response = {}
|
||||
try:
|
||||
response = requests.post(
|
||||
self._url,
|
||||
json={"base64": encode_image(image)},
|
||||
timeout=TIMEOUT
|
||||
).json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
|
||||
response['success'] = False
|
||||
|
||||
if response['success']:
|
||||
total_faces = response['facesCount']
|
||||
faces = parse_faces(response['faces'])
|
||||
self._matched = get_matched_faces(faces)
|
||||
self.process_faces(faces, total_faces)
|
||||
response = post_image(self._url_check, image)
|
||||
if response is not None:
|
||||
response_json = response.json()
|
||||
if response_json['success']:
|
||||
total_faces = response_json['facesCount']
|
||||
faces = parse_faces(response_json['faces'])
|
||||
self._matched = get_matched_faces(faces)
|
||||
self.process_faces(faces, total_faces)
|
||||
|
||||
else:
|
||||
self.total_faces = None
|
||||
self.faces = []
|
||||
self._matched = {}
|
||||
|
||||
def teach(self, name, file_path):
|
||||
"""Teach classifier a face name."""
|
||||
if (not self.hass.config.is_allowed_path(file_path)
|
||||
or not valid_file_path(file_path)):
|
||||
return
|
||||
with open(file_path, 'rb') as open_file:
|
||||
response = requests.post(
|
||||
self._url_teach,
|
||||
data={ATTR_NAME: name, 'id': file_path},
|
||||
files={'file': open_file})
|
||||
|
||||
if response.status_code == 200:
|
||||
self.hass.bus.fire(
|
||||
EVENT_CLASSIFIER_TEACH, {
|
||||
ATTR_CLASSIFIER: CLASSIFIER,
|
||||
ATTR_NAME: name,
|
||||
FILE_PATH: file_path,
|
||||
'success': True,
|
||||
'message': None
|
||||
})
|
||||
|
||||
elif response.status_code == 400:
|
||||
_LOGGER.warning(
|
||||
"%s teaching of file %s failed with message:%s",
|
||||
CLASSIFIER, file_path, response.text)
|
||||
self.hass.bus.fire(
|
||||
EVENT_CLASSIFIER_TEACH, {
|
||||
ATTR_CLASSIFIER: CLASSIFIER,
|
||||
ATTR_NAME: name,
|
||||
FILE_PATH: file_path,
|
||||
'success': False,
|
||||
'message': response.text
|
||||
})
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
"""Return camera entity id from process pictures."""
|
||||
@ -131,4 +217,5 @@ class FaceClassifyEntity(ImageProcessingFaceEntity):
|
||||
"""Return the classifier attributes."""
|
||||
return {
|
||||
'matched_faces': self._matched,
|
||||
'total_matched_faces': len(self._matched),
|
||||
}
|
||||
|
@ -6,3 +6,16 @@ scan:
|
||||
entity_id:
|
||||
description: Name(s) of entities to scan immediately.
|
||||
example: 'image_processing.alpr_garage'
|
||||
|
||||
facebox_teach_face:
|
||||
description: Teach facebox a face using a file.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The facebox entity to teach.
|
||||
example: 'image_processing.facebox'
|
||||
name:
|
||||
description: The name of the face to teach.
|
||||
example: 'my_name'
|
||||
file_path:
|
||||
description: The path to the image file.
|
||||
example: '/images/my_image.jpg'
|
||||
|
@ -174,7 +174,7 @@ class DeconzLight(Light):
|
||||
data = {'on': False}
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
data = {'bri': 0}
|
||||
data['bri'] = 0
|
||||
data['transitiontime'] = int(kwargs[ATTR_TRANSITION]) * 10
|
||||
|
||||
if ATTR_FLASH in kwargs:
|
||||
|
@ -36,7 +36,6 @@ class EufyLight(Light):
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the light."""
|
||||
# pylint: disable=import-error
|
||||
import lakeside
|
||||
|
||||
self._temp = None
|
||||
|
@ -218,6 +218,9 @@ class FluxLight(Light):
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the specified or all lights on."""
|
||||
if not self.is_on:
|
||||
self._bulb.turnOn()
|
||||
|
||||
hs_color = kwargs.get(ATTR_HS_COLOR)
|
||||
|
||||
if hs_color:
|
||||
@ -269,9 +272,6 @@ class FluxLight(Light):
|
||||
else:
|
||||
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
|
||||
|
||||
if not self.is_on:
|
||||
self._bulb.turnOn()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the specified or all lights off."""
|
||||
self._bulb.turnOff()
|
||||
|
@ -7,33 +7,39 @@ https://home-assistant.io/components/light.homematicip_cloud/
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS)
|
||||
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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_POWER_CONSUMPTION = 'power_consumption'
|
||||
ATTR_ENERGIE_COUNTER = 'energie_counter'
|
||||
ATTR_ENERGIE_COUNTER = 'energie_counter_kwh'
|
||||
ATTR_PROFILE_MODE = 'profile_mode'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP light devices."""
|
||||
from homematicip.device import (
|
||||
BrandSwitchMeasuring)
|
||||
"""Old way of setting up HomematicIP lights."""
|
||||
pass
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP lights from a config entry."""
|
||||
from homematicip.aio.device import (
|
||||
AsyncBrandSwitchMeasuring, AsyncDimmer)
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, BrandSwitchMeasuring):
|
||||
if isinstance(device, AsyncBrandSwitchMeasuring):
|
||||
devices.append(HomematicipLightMeasuring(home, device))
|
||||
elif isinstance(device, AsyncDimmer):
|
||||
devices.append(HomematicipDimmer(home, device))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
@ -64,13 +70,50 @@ class HomematicipLightMeasuring(HomematicipLight):
|
||||
"""MomematicIP measuring light device."""
|
||||
|
||||
@property
|
||||
def current_power_w(self):
|
||||
"""Return the current power usage in W."""
|
||||
return self._device.currentPowerConsumption
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the generic device."""
|
||||
attr = super().device_state_attributes
|
||||
if self._device.currentPowerConsumption > 0.05:
|
||||
attr.update({
|
||||
ATTR_POWER_CONSUMPTION:
|
||||
round(self._device.currentPowerConsumption, 2)
|
||||
})
|
||||
attr.update({
|
||||
ATTR_ENERGIE_COUNTER: round(self._device.energyCounter, 2)
|
||||
})
|
||||
return attr
|
||||
|
||||
|
||||
class HomematicipDimmer(HomematicipGenericDevice, Light):
|
||||
"""MomematicIP dimmer light device."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the dimmer light device."""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def today_energy_kwh(self):
|
||||
"""Return the today total energy usage in kWh."""
|
||||
if self._device.energyCounter is None:
|
||||
return 0
|
||||
return round(self._device.energyCounter)
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._device.dimLevel != 0
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return int(self._device.dimLevel*255)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_BRIGHTNESS
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
await self._device.set_dim_level(
|
||||
kwargs[ATTR_BRIGHTNESS]/255.0)
|
||||
else:
|
||||
await self._device.set_dim_level(1)
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
await self._device.set_dim_level(0)
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.util.color import (
|
||||
color_temperature_mired_to_kelvin, color_hs_to_RGB)
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
REQUIREMENTS = ['limitlessled==1.1.0']
|
||||
REQUIREMENTS = ['limitlessled==1.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -46,7 +46,7 @@ MIN_SATURATION = 10
|
||||
WHITE = [0, 0]
|
||||
|
||||
SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
|
||||
SUPPORT_TRANSITION)
|
||||
SUPPORT_EFFECT | SUPPORT_TRANSITION)
|
||||
SUPPORT_LIMITLESSLED_DIMMER = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
|
||||
SUPPORT_LIMITLESSLED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
|
||||
SUPPORT_FLASH | SUPPORT_COLOR |
|
||||
@ -239,6 +239,8 @@ class LimitlessLEDGroup(Light):
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the temperature property."""
|
||||
if self.hs_color is not None:
|
||||
return None
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
@ -247,6 +249,9 @@ class LimitlessLEDGroup(Light):
|
||||
if self._effect == EFFECT_NIGHT:
|
||||
return None
|
||||
|
||||
if self._color is None or self._color[1] == 0:
|
||||
return None
|
||||
|
||||
return self._color
|
||||
|
||||
@property
|
||||
|
@ -155,7 +155,11 @@ class MyStromLight(Light):
|
||||
self._state = self._bulb.get_status()
|
||||
|
||||
colors = self._bulb.get_color()['color']
|
||||
color_h, color_s, color_v = colors.split(';')
|
||||
try:
|
||||
color_h, color_s, color_v = colors.split(';')
|
||||
except ValueError:
|
||||
color_s, color_v = colors.split(';')
|
||||
color_h = 0
|
||||
|
||||
self._color_h = int(color_h)
|
||||
self._color_s = int(color_s)
|
||||
|
@ -66,6 +66,8 @@ class TPLinkSmartBulb(Light):
|
||||
self._brightness = None
|
||||
self._hs = None
|
||||
self._supported_features = 0
|
||||
self._min_mireds = None
|
||||
self._max_mireds = None
|
||||
self._emeter_params = {}
|
||||
|
||||
@property
|
||||
@ -107,12 +109,12 @@ class TPLinkSmartBulb(Light):
|
||||
@property
|
||||
def min_mireds(self):
|
||||
"""Return minimum supported color temperature."""
|
||||
return kelvin_to_mired(self.smartbulb.valid_temperature_range[1])
|
||||
return self._min_mireds
|
||||
|
||||
@property
|
||||
def max_mireds(self):
|
||||
"""Return maximum supported color temperature."""
|
||||
return kelvin_to_mired(self.smartbulb.valid_temperature_range[0])
|
||||
return self._max_mireds
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
@ -195,5 +197,9 @@ class TPLinkSmartBulb(Light):
|
||||
self._supported_features += SUPPORT_BRIGHTNESS
|
||||
if self.smartbulb.is_variable_color_temp:
|
||||
self._supported_features += SUPPORT_COLOR_TEMP
|
||||
self._min_mireds = kelvin_to_mired(
|
||||
self.smartbulb.valid_temperature_range[1])
|
||||
self._max_mireds = kelvin_to_mired(
|
||||
self.smartbulb.valid_temperature_range[0])
|
||||
if self.smartbulb.is_color:
|
||||
self._supported_features += SUPPORT_COLOR
|
||||
|
102
homeassistant/components/light/tuya.py
Normal file
102
homeassistant/components/light/tuya.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""
|
||||
Support for the Tuya light.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.tuya/
|
||||
"""
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ENTITY_ID_FORMAT,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light)
|
||||
|
||||
from homeassistant.components.tuya import DATA_TUYA, TuyaDevice
|
||||
from homeassistant.util import color as colorutil
|
||||
|
||||
DEPENDENCIES = ['tuya']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Tuya light platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
tuya = hass.data[DATA_TUYA]
|
||||
dev_ids = discovery_info.get('dev_ids')
|
||||
devices = []
|
||||
for dev_id in dev_ids:
|
||||
device = tuya.get_device_by_id(dev_id)
|
||||
if device is None:
|
||||
continue
|
||||
devices.append(TuyaLight(device))
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class TuyaLight(TuyaDevice, Light):
|
||||
"""Tuya light device."""
|
||||
|
||||
def __init__(self, tuya):
|
||||
"""Init Tuya light device."""
|
||||
super().__init__(tuya)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of the light."""
|
||||
return self.tuya.brightness()
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the hs_color of the light."""
|
||||
return self.tuya.hs_color()
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color_temp of the light."""
|
||||
color_temp = self.tuya.color_temp()
|
||||
if color_temp is None:
|
||||
return None
|
||||
return colorutil.color_temperature_kelvin_to_mired(color_temp)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self.tuya.state()
|
||||
|
||||
@property
|
||||
def min_mireds(self):
|
||||
"""Return color temperature min mireds."""
|
||||
return colorutil.color_temperature_kelvin_to_mired(
|
||||
self.tuya.min_color_temp())
|
||||
|
||||
@property
|
||||
def max_mireds(self):
|
||||
"""Return color temperature max mireds."""
|
||||
return colorutil.color_temperature_kelvin_to_mired(
|
||||
self.tuya.max_color_temp())
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn on or control the light."""
|
||||
if (ATTR_BRIGHTNESS not in kwargs
|
||||
and ATTR_HS_COLOR not in kwargs
|
||||
and ATTR_COLOR_TEMP not in kwargs):
|
||||
self.tuya.turn_on()
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self.tuya.set_brightness(kwargs[ATTR_BRIGHTNESS])
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
self.tuya.set_color(kwargs[ATTR_HS_COLOR])
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_temp = colorutil.color_temperature_mired_to_kelvin(
|
||||
kwargs[ATTR_COLOR_TEMP])
|
||||
self.tuya.set_color_temp(color_temp)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Instruct the light to turn off."""
|
||||
self.tuya.turn_off()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supports = SUPPORT_BRIGHTNESS
|
||||
if self.tuya.support_color():
|
||||
supports = supports | SUPPORT_COLOR
|
||||
if self.tuya.support_color_temp():
|
||||
supports = supports | SUPPORT_COLOR_TEMP
|
||||
return supports
|
@ -31,7 +31,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light):
|
||||
"""Initialize the XiaomiGatewayLight."""
|
||||
self._data_key = 'rgb'
|
||||
self._hs = (0, 0)
|
||||
self._brightness = 180
|
||||
self._brightness = 100
|
||||
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
|
||||
@ -64,7 +64,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light):
|
||||
brightness = rgba[0]
|
||||
rgb = rgba[1:]
|
||||
|
||||
self._brightness = int(255 * brightness / 100)
|
||||
self._brightness = brightness
|
||||
self._hs = color_util.color_RGB_to_hs(*rgb)
|
||||
self._state = True
|
||||
return True
|
||||
@ -72,7 +72,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light):
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
return int(255 * self._brightness / 100)
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
|
@ -310,7 +310,7 @@ class YeelightLight(Light):
|
||||
|
||||
bright = self._properties.get('bright', None)
|
||||
if bright:
|
||||
self._brightness = 255 * (int(bright) / 100)
|
||||
self._brightness = round(255 * (int(bright) / 100))
|
||||
|
||||
temp_in_k = self._properties.get('ct', None)
|
||||
if temp_in_k:
|
||||
|
@ -324,9 +324,11 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
else:
|
||||
self._ct = TEMP_COLD_HASS
|
||||
rgbw = '#00000000ff'
|
||||
|
||||
elif ATTR_HS_COLOR in kwargs:
|
||||
self._hs = kwargs[ATTR_HS_COLOR]
|
||||
if ATTR_WHITE_VALUE not in kwargs:
|
||||
# white LED must be off in order for color to work
|
||||
self._white = 0
|
||||
|
||||
if ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs:
|
||||
rgbw = '#'
|
||||
|
@ -83,7 +83,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None):
|
||||
hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data)
|
||||
|
||||
|
||||
async def setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Listen for download events to download files."""
|
||||
@callback
|
||||
def log_message(service):
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
|
||||
SERVICE_PLAY_MEDIA)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['youtube_dl==2018.06.25']
|
||||
REQUIREMENTS = ['youtube_dl==2018.07.04']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -12,19 +12,19 @@ import voluptuous as vol
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
||||
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON,
|
||||
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY)
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, MediaPlayerDevice,
|
||||
PLATFORM_SCHEMA, SUPPORT_TURN_ON, MEDIA_TYPE_MUSIC,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_PLAY)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
||||
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['denonavr==0.7.3']
|
||||
REQUIREMENTS = ['denonavr==0.7.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = None
|
||||
DEFAULT_SHOW_SOURCES = False
|
||||
DEFAULT_TIMEOUT = 2
|
||||
CONF_SHOW_ALL_SOURCES = 'show_all_sources'
|
||||
@ -33,6 +33,8 @@ CONF_VALID_ZONES = ['Zone2', 'Zone3']
|
||||
CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)'
|
||||
KEY_DENON_CACHE = 'denonavr_hosts'
|
||||
|
||||
ATTR_SOUND_MODE_RAW = 'sound_mode_raw'
|
||||
|
||||
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
||||
SUPPORT_SELECT_SOURCE | SUPPORT_VOLUME_SET
|
||||
@ -146,6 +148,20 @@ class DenonDevice(MediaPlayerDevice):
|
||||
self._frequency = self._receiver.frequency
|
||||
self._station = self._receiver.station
|
||||
|
||||
self._sound_mode_support = self._receiver.support_sound_mode
|
||||
if self._sound_mode_support:
|
||||
self._sound_mode = self._receiver.sound_mode
|
||||
self._sound_mode_raw = self._receiver.sound_mode_raw
|
||||
self._sound_mode_list = self._receiver.sound_mode_list
|
||||
else:
|
||||
self._sound_mode = None
|
||||
self._sound_mode_raw = None
|
||||
self._sound_mode_list = None
|
||||
|
||||
self._supported_features_base = SUPPORT_DENON
|
||||
self._supported_features_base |= (self._sound_mode_support and
|
||||
SUPPORT_SELECT_SOUND_MODE)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest status information from device."""
|
||||
self._receiver.update()
|
||||
@ -163,6 +179,9 @@ class DenonDevice(MediaPlayerDevice):
|
||||
self._band = self._receiver.band
|
||||
self._frequency = self._receiver.frequency
|
||||
self._station = self._receiver.station
|
||||
if self._sound_mode_support:
|
||||
self._sound_mode = self._receiver.sound_mode
|
||||
self._sound_mode_raw = self._receiver.sound_mode_raw
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -196,12 +215,22 @@ class DenonDevice(MediaPlayerDevice):
|
||||
"""Return a list of available input sources."""
|
||||
return self._source_list
|
||||
|
||||
@property
|
||||
def sound_mode(self):
|
||||
"""Return the current matched sound mode."""
|
||||
return self._sound_mode
|
||||
|
||||
@property
|
||||
def sound_mode_list(self):
|
||||
"""Return a list of available sound modes."""
|
||||
return self._sound_mode_list
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag media player features that are supported."""
|
||||
if self._current_source in self._receiver.netaudio_func_list:
|
||||
return SUPPORT_DENON | SUPPORT_MEDIA_MODES
|
||||
return SUPPORT_DENON
|
||||
return self._supported_features_base | SUPPORT_MEDIA_MODES
|
||||
return self._supported_features_base
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
@ -275,6 +304,15 @@ class DenonDevice(MediaPlayerDevice):
|
||||
"""Episode of current playing media, TV show only."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attributes = {}
|
||||
if (self._sound_mode_raw is not None and self._sound_mode_support and
|
||||
self._power == 'ON'):
|
||||
attributes[ATTR_SOUND_MODE_RAW] = self._sound_mode_raw
|
||||
return attributes
|
||||
|
||||
def media_play_pause(self):
|
||||
"""Simulate play pause media player."""
|
||||
return self._receiver.toggle_play_pause()
|
||||
@ -291,6 +329,10 @@ class DenonDevice(MediaPlayerDevice):
|
||||
"""Select input source."""
|
||||
return self._receiver.set_input_func(source)
|
||||
|
||||
def select_sound_mode(self, sound_mode):
|
||||
"""Select sound mode."""
|
||||
return self._receiver.set_sound_mode(sound_mode)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on media player."""
|
||||
if self._receiver.power_on():
|
||||
|
@ -20,8 +20,7 @@ from homeassistant.const import (
|
||||
STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
|
||||
import homeassistant.util as util
|
||||
|
||||
REQUIREMENTS = ['https://github.com/wokar/pylgnetcast/archive/'
|
||||
'v0.2.0.zip#pylgnetcast==0.2.0']
|
||||
REQUIREMENTS = ['pylgnetcast-homeassistant==0.2.0.dev0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -88,6 +88,8 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
|
||||
import pyteleloisirs
|
||||
try:
|
||||
self._state = self.refresh_state()
|
||||
# Update channel list
|
||||
self.refresh_channel_list()
|
||||
# Update current channel
|
||||
channel = self._client.channel
|
||||
if channel is not None:
|
||||
|
@ -22,7 +22,7 @@ from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
STATE_IDLE)
|
||||
from homeassistant import util
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
REQUIREMENTS = ['pexpect==4.6.0']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# SUPPORT_VOLUME_SET is close to available but we need volume up/down
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.script import Script
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['ha-philipsjs==0.0.4']
|
||||
REQUIREMENTS = ['ha-philipsjs==0.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user