Merge pull request #15570 from home-assistant/rc

0.74
This commit is contained in:
Paulus Schoutsen 2018-07-20 15:11:18 +02:00 committed by GitHub
commit da3366859d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
232 changed files with 6447 additions and 2202 deletions

View File

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

@ -0,0 +1,2 @@
[settings]
multi_line_output=4

View File

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

View File

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

View File

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

View 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

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

View File

@ -0,0 +1,4 @@
"""Constants for the auth module."""
from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)

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

View 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 {}

View File

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

View File

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

View File

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

View 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')

View File

@ -1 +0,0 @@
"""Auth providers for Home Assistant."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View File

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

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

View File

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

View 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
]
}

View 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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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'], {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)"
}
},
"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"
}
}
}

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

View 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
}
)

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

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

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

View 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

View 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![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)"
}
},
"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"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,6 @@ class EufyLight(Light):
def __init__(self, device):
"""Initialize the light."""
# pylint: disable=import-error
import lakeside
self._temp = None

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2018.06.25']
REQUIREMENTS = ['youtube_dl==2018.07.04']
_LOGGER = logging.getLogger(__name__)

View File

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

View File

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

View File

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

View File

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

View File

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