Merge remote-tracking branch 'upstream/dev' into igd

This commit is contained in:
Steven Looman 2018-08-24 15:25:07 +02:00
commit e73f31d829
485 changed files with 6059 additions and 3434 deletions

View File

@ -104,6 +104,9 @@ omit =
homeassistant/components/fritzbox.py
homeassistant/components/switch/fritzbox.py
homeassistant/components/ecovacs.py
homeassistant/components/*/ecovacs.py
homeassistant/components/eufy.py
homeassistant/components/*/eufy.py
@ -113,6 +116,12 @@ omit =
homeassistant/components/google.py
homeassistant/components/*/google.py
homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
homeassistant/components/*/hangouts.py
homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py
@ -133,12 +142,13 @@ omit =
homeassistant/components/ihc/*
homeassistant/components/*/ihc.py
homeassistant/components/insteon/*
homeassistant/components/*/insteon.py
homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py
homeassistant/components/insteon_plm/*
homeassistant/components/*/insteon_plm.py
homeassistant/components/insteon_plm.py
homeassistant/components/ios.py
homeassistant/components/*/ios.py
@ -685,6 +695,7 @@ omit =
homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/netdata_public.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/noaa_tides.py
homeassistant/components/sensor/nsw_fuel_station.py
homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.py

View File

@ -87,6 +87,8 @@ homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610
homeassistant/components/ecovacs.py @OverloadUT
homeassistant/components/*/ecovacs.py @OverloadUT
homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline

View File

@ -60,14 +60,6 @@ loader module
:undoc-members:
:show-inheritance:
remote module
---------------------------
.. automodule:: homeassistant.remote
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -2,23 +2,30 @@
import asyncio
import logging
from collections import OrderedDict
from typing import List, Awaitable
from typing import Any, Dict, List, Optional, Tuple, cast
import jwt
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.core import callback, HomeAssistant
from homeassistant.util import dt as dt_util
from . import auth_store
from .providers import auth_provider_from_config
from . import auth_store, models
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
_LOGGER = logging.getLogger(__name__)
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
_ProviderKey = Tuple[str, Optional[str]]
_ProviderDict = Dict[_ProviderKey, AuthProvider]
async def auth_manager_from_config(
hass: HomeAssistant,
provider_configs: List[dict]) -> Awaitable['AuthManager']:
provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]]) -> 'AuthManager':
"""Initialize an auth manager from config."""
store = auth_store.AuthStore(hass)
if provider_configs:
@ -26,9 +33,9 @@ async def auth_manager_from_config(
*[auth_provider_from_config(hass, store, config)
for config in provider_configs])
else:
providers = []
providers = ()
# So returned auth providers are in same order as config
provider_hash = OrderedDict()
provider_hash = OrderedDict() # type: _ProviderDict
for provider in providers:
if provider is None:
continue
@ -42,28 +49,53 @@ async def auth_manager_from_config(
continue
provider_hash[key] = provider
manager = AuthManager(hass, store, provider_hash)
if module_configs:
modules = await asyncio.gather(
*[auth_mfa_module_from_config(hass, config)
for config in module_configs])
else:
modules = ()
# So returned auth modules are in same order as config
module_hash = OrderedDict() # type: _MfaModuleDict
for module in modules:
if module is None:
continue
if module.id in module_hash:
_LOGGER.error(
'Found duplicate multi-factor module: %s. Please add unique '
'IDs if you want to have the same module twice.', module.id)
continue
module_hash[module.id] = module
manager = AuthManager(hass, store, provider_hash, module_hash)
return manager
class AuthManager:
"""Manage the authentication for Home Assistant."""
def __init__(self, hass, store, providers):
def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore,
providers: _ProviderDict, mfa_modules: _MfaModuleDict) \
-> None:
"""Initialize the auth manager."""
self.hass = hass
self._store = store
self._providers = providers
self._mfa_modules = mfa_modules
self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow,
self._async_finish_login_flow)
@property
def active(self):
def active(self) -> bool:
"""Return if any auth providers are registered."""
return bool(self._providers)
@property
def support_legacy(self):
def support_legacy(self) -> bool:
"""
Return if legacy_api_password auth providers are registered.
@ -75,19 +107,39 @@ class AuthManager:
return False
@property
def auth_providers(self):
def auth_providers(self) -> List[AuthProvider]:
"""Return a list of available auth providers."""
return list(self._providers.values())
async def async_get_users(self):
@property
def auth_mfa_modules(self) -> List[MultiFactorAuthModule]:
"""Return a list of available auth modules."""
return list(self._mfa_modules.values())
def get_auth_mfa_module(self, module_id: str) \
-> Optional[MultiFactorAuthModule]:
"""Return an multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id)
async def async_get_users(self) -> List[models.User]:
"""Retrieve all users."""
return await self._store.async_get_users()
async def async_get_user(self, user_id):
async def async_get_user(self, user_id: str) -> Optional[models.User]:
"""Retrieve a user."""
return await self._store.async_get_user(user_id)
async def async_create_system_user(self, name):
async def async_get_user_by_credentials(
self, credentials: models.Credentials) -> Optional[models.User]:
"""Get a user by credential, return None if not found."""
for user in await self.async_get_users():
for creds in user.credentials:
if creds.id == credentials.id:
return user
return None
async def async_create_system_user(self, name: str) -> models.User:
"""Create a system user."""
return await self._store.async_create_user(
name=name,
@ -95,27 +147,27 @@ class AuthManager:
is_active=True,
)
async def async_create_user(self, name):
async def async_create_user(self, name: str) -> models.User:
"""Create a user."""
kwargs = {
'name': name,
'is_active': True,
}
} # type: Dict[str, Any]
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):
async def async_get_or_create_user(self, credentials: models.Credentials) \
-> models.User:
"""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.')
user = await self.async_get_user_by_credentials(credentials)
if user is None:
raise ValueError('Unable to find the user.')
else:
return user
auth_provider = self._async_get_auth_provider(credentials)
@ -127,15 +179,16 @@ class AuthManager:
return await self._store.async_create_user(
credentials=credentials,
name=info.get('name'),
is_active=info.get('is_active', False)
name=info.name,
is_active=info.is_active,
)
async def async_link_user(self, user, credentials):
async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None:
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
async def async_remove_user(self, user):
async def async_remove_user(self, user: models.User) -> None:
"""Remove a user."""
tasks = [
self.async_remove_credentials(credentials)
@ -147,27 +200,75 @@ class AuthManager:
await self._store.async_remove_user(user)
async def async_activate_user(self, user):
async def async_activate_user(self, user: models.User) -> None:
"""Activate a user."""
await self._store.async_activate_user(user)
async def async_deactivate_user(self, user):
async def async_deactivate_user(self, user: models.User) -> None:
"""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):
async def async_remove_credentials(
self, credentials: models.Credentials) -> None:
"""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)
# https://github.com/python/mypy/issues/1424
await provider.async_will_remove_credentials( # type: ignore
credentials)
await self._store.async_remove_credentials(credentials)
async def async_create_refresh_token(self, user, client_id=None):
async def async_enable_user_mfa(self, user: models.User,
mfa_module_id: str, data: Any) -> None:
"""Enable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError('System generated users cannot enable '
'multi-factor auth module.')
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))
if module.setup_schema is not None:
try:
# pylint: disable=not-callable
data = module.setup_schema(data)
except vol.Invalid as err:
raise ValueError('Data does not match schema: {}'.format(err))
await module.async_setup_user(user.id, data)
async def async_disable_user_mfa(self, user: models.User,
mfa_module_id: str) -> None:
"""Disable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError('System generated users cannot disable '
'multi-factor auth module.')
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))
await module.async_depose_user(user.id)
async def async_get_enabled_mfa(self, user: models.User) -> List[str]:
"""List enabled mfa modules for user."""
module_ids = []
for module_id, module in self._mfa_modules.items():
if await module.async_is_user_setup(user.id):
module_ids.append(module_id)
return module_ids
async def async_create_refresh_token(self, user: models.User,
client_id: Optional[str] = None) \
-> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
raise ValueError('User is not active')
@ -182,16 +283,25 @@ class AuthManager:
return await self._store.async_create_refresh_token(user, client_id)
async def async_get_refresh_token(self, token_id):
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token(self, token):
async def async_get_refresh_token_by_token(
self, token: str) -> Optional[models.RefreshToken]:
"""Get refresh token by token."""
return await self._store.async_get_refresh_token_by_token(token)
async def async_remove_refresh_token(self,
refresh_token: models.RefreshToken) \
-> None:
"""Delete a refresh token."""
await self._store.async_remove_refresh_token(refresh_token)
@callback
def async_create_access_token(self, refresh_token):
def async_create_access_token(self,
refresh_token: models.RefreshToken) -> str:
"""Create a new access token."""
# pylint: disable=no-self-use
return jwt.encode({
@ -200,15 +310,16 @@ class AuthManager:
'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode()
async def async_validate_access_token(self, token):
"""Return if an access token is valid."""
async def async_validate_access_token(
self, token: str) -> Optional[models.RefreshToken]:
"""Return refresh token if an access token is valid."""
try:
unverif_claims = jwt.decode(token, verify=False)
except jwt.InvalidTokenError:
return None
refresh_token = await self.async_get_refresh_token(
unverif_claims.get('iss'))
cast(str, unverif_claims.get('iss')))
if refresh_token is None:
jwt_key = ''
@ -228,34 +339,63 @@ class AuthManager:
except jwt.InvalidTokenError:
return None
if not refresh_token.user.is_active:
if refresh_token is None or not refresh_token.user.is_active:
return None
return refresh_token
async def _async_create_login_flow(self, handler, *, context, data):
async def _async_create_login_flow(
self, handler: _ProviderKey, *, context: Optional[Dict],
data: Optional[Any]) -> data_entry_flow.FlowHandler:
"""Create a login flow."""
auth_provider = self._providers[handler]
return await auth_provider.async_credential_flow(context)
return await auth_provider.async_login_flow(context)
async def _async_finish_login_flow(self, context, result):
"""Result of a credential login flow."""
async def _async_finish_login_flow(
self, flow: LoginFlow, result: Dict[str, Any]) \
-> Dict[str, Any]:
"""Return a user as result of login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return None
return result
# we got final result
if isinstance(result['data'], models.User):
result['result'] = result['data']
return result
auth_provider = self._providers[result['handler']]
return await auth_provider.async_get_or_create_credentials(
credentials = await auth_provider.async_get_or_create_credentials(
result['data'])
if flow.context is not None and flow.context.get('credential_only'):
result['result'] = credentials
return result
# multi-factor module cannot enabled for new credential
# which has not linked to a user yet
if auth_provider.support_mfa and not credentials.is_new:
user = await self.async_get_user_by_credentials(credentials)
if user is not None:
modules = await self.async_get_enabled_mfa(user)
if modules:
flow.user = user
flow.available_mfa_modules = modules
return await flow.async_step_select_mfa_module()
result['result'] = await self.async_get_or_create_user(credentials)
return result
@callback
def _async_get_auth_provider(self, credentials):
"""Helper to get auth provider from a set of credentials."""
def _async_get_auth_provider(
self, credentials: models.Credentials) -> Optional[AuthProvider]:
"""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):
async def _user_should_be_owner(self) -> bool:
"""Determine if user should be owner.
A user should be an owner if it is the first non-system user that is

View File

@ -1,8 +1,11 @@
"""Storage for auth models."""
from collections import OrderedDict
from datetime import timedelta
from logging import getLogger
from typing import Any, Dict, List, Optional # noqa: F401
import hmac
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import dt as dt_util
from . import models
@ -20,35 +23,41 @@ class AuthStore:
called that needs it.
"""
def __init__(self, hass):
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the auth store."""
self.hass = hass
self._users = None
self._users = None # type: Optional[Dict[str, models.User]]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
async def async_get_users(self):
async def async_get_users(self) -> List[models.User]:
"""Retrieve all users."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
return list(self._users.values())
async def async_get_user(self, user_id):
async def async_get_user(self, user_id: str) -> Optional[models.User]:
"""Retrieve a user by id."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
return self._users.get(user_id)
async def async_create_user(self, name, is_owner=None, is_active=None,
system_generated=None, credentials=None):
async def async_create_user(
self, name: Optional[str], is_owner: Optional[bool] = None,
is_active: Optional[bool] = None,
system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None) -> models.User:
"""Create a new user."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
kwargs = {
'name': name
}
} # type: Dict[str, Any]
if is_owner is not None:
kwargs['is_owner'] = is_owner
@ -64,36 +73,46 @@ class AuthStore:
self._users[new_user.id] = new_user
if credentials is None:
await self.async_save()
self._async_schedule_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):
async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None:
"""Add credentials to an existing user."""
user.credentials.append(credentials)
await self.async_save()
self._async_schedule_save()
credentials.is_new = False
async def async_remove_user(self, user):
async def async_remove_user(self, user: models.User) -> None:
"""Remove a user."""
self._users.pop(user.id)
await self.async_save()
if self._users is None:
await self._async_load()
assert self._users is not None
async def async_activate_user(self, user):
self._users.pop(user.id)
self._async_schedule_save()
async def async_activate_user(self, user: models.User) -> None:
"""Activate a user."""
user.is_active = True
await self.async_save()
self._async_schedule_save()
async def async_deactivate_user(self, user):
async def async_deactivate_user(self, user: models.User) -> None:
"""Activate a user."""
user.is_active = False
await self.async_save()
self._async_schedule_save()
async def async_remove_credentials(self, credentials):
async def async_remove_credentials(
self, credentials: models.Credentials) -> None:
"""Remove credentials."""
if self._users is None:
await self._async_load()
assert self._users is not None
for user in self._users.values():
found = None
@ -106,19 +125,35 @@ class AuthStore:
user.credentials.pop(found)
break
await self.async_save()
self._async_schedule_save()
async def async_create_refresh_token(self, user, client_id=None):
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None) \
-> models.RefreshToken:
"""Create a new token for a user."""
refresh_token = models.RefreshToken(user=user, client_id=client_id)
user.refresh_tokens[refresh_token.id] = refresh_token
await self.async_save()
self._async_schedule_save()
return refresh_token
async def async_get_refresh_token(self, token_id):
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken) -> None:
"""Remove a refresh token."""
if self._users is None:
await self._async_load()
assert self._users is not None
for user in self._users.values():
if user.refresh_tokens.pop(refresh_token.id, None):
self._async_schedule_save()
break
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
for user in self._users.values():
refresh_token = user.refresh_tokens.get(token_id)
@ -127,10 +162,12 @@ class AuthStore:
return None
async def async_get_refresh_token_by_token(self, token):
async def async_get_refresh_token_by_token(
self, token: str) -> Optional[models.RefreshToken]:
"""Get refresh token by token."""
if self._users is None:
await self.async_load()
await self._async_load()
assert self._users is not None
found = None
@ -141,7 +178,7 @@ class AuthStore:
return found
async def async_load(self):
async def _async_load(self) -> None:
"""Load the users."""
data = await self._store.async_load()
@ -150,7 +187,7 @@ class AuthStore:
if self._users is not None:
return
users = OrderedDict()
users = OrderedDict() # type: Dict[str, models.User]
if data is None:
self._users = users
@ -173,11 +210,17 @@ class AuthStore:
if 'jwt_key' not in rt_dict:
continue
created_at = dt_util.parse_datetime(rt_dict['created_at'])
if created_at is None:
getLogger(__name__).error(
'Ignoring refresh token %(id)s with invalid created_at '
'%(created_at)s for user_id %(user_id)s', rt_dict)
continue
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']),
created_at=created_at,
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
@ -187,8 +230,19 @@ class AuthStore:
self._users = users
async def async_save(self):
@callback
def _async_schedule_save(self) -> None:
"""Save users."""
if self._users is None:
return
self._store.async_delay_save(self._data_to_save, 1)
@callback
def _data_to_save(self) -> Dict:
"""Return the data to store."""
assert self._users is not None
users = [
{
'id': user.id,
@ -227,10 +281,8 @@ class AuthStore:
for refresh_token in user.refresh_tokens.values()
]
data = {
return {
'users': users,
'credentials': credentials,
'refresh_tokens': refresh_tokens,
}
await self._store.async_save(data, delay=1)

View File

@ -0,0 +1,141 @@
"""Plugable auth modules for Home Assistant."""
from datetime import timedelta
import importlib
import logging
import types
from typing import Any, Dict, Optional
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant import requirements
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.util.decorator import Registry
MULTI_FACTOR_AUTH_MODULES = Registry()
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): str,
vol.Optional(CONF_NAME): str,
# Specify ID if you have two mfa auth module for same type.
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
SESSION_EXPIRATION = timedelta(minutes=5)
DATA_REQS = 'mfa_auth_module_reqs_processed'
_LOGGER = logging.getLogger(__name__)
class MultiFactorAuthModule:
"""Multi-factor Auth Module of validation function."""
DEFAULT_TITLE = 'Unnamed auth module'
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize an auth module."""
self.hass = hass
self.config = config
@property
def id(self) -> str: # pylint: disable=invalid-name
"""Return id of the auth module.
Default is same as type
"""
return self.config.get(CONF_ID, self.type)
@property
def type(self) -> str:
"""Return type of the module."""
return self.config[CONF_TYPE] # type: ignore
@property
def name(self) -> str:
"""Return the name of the auth module."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
# Implement by extending class
@property
def input_schema(self) -> vol.Schema:
"""Return a voluptuous schema to define mfa auth module's input."""
raise NotImplementedError
@property
def setup_schema(self) -> Optional[vol.Schema]:
"""Return a vol schema to validate mfa auth module's setup input.
Optional
"""
return None
async def async_setup_user(self, user_id: str, setup_data: Any) -> None:
"""Set up user for mfa auth module."""
raise NotImplementedError
async def async_depose_user(self, user_id: str) -> None:
"""Remove user from mfa module."""
raise NotImplementedError
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
raise NotImplementedError
async def async_validation(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
raise NotImplementedError
async def auth_mfa_module_from_config(
hass: HomeAssistant, config: Dict[str, Any]) \
-> Optional[MultiFactorAuthModule]:
"""Initialize an auth module from a config."""
module_name = config[CONF_TYPE]
module = await _load_mfa_module(hass, module_name)
if module is None:
return None
try:
config = module.CONFIG_SCHEMA(config) # type: ignore
except vol.Invalid as err:
_LOGGER.error('Invalid configuration for multi-factor module %s: %s',
module_name, humanize_error(config, err))
return None
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
-> Optional[types.ModuleType]:
"""Load an mfa auth module."""
module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name)
try:
module = importlib.import_module(module_path)
except ImportError:
_LOGGER.warning('Unable to find %s', module_path)
return None
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
return module
processed = hass.data.get(DATA_REQS)
if processed and module_name in processed:
return module
processed = hass.data[DATA_REQS] = set()
# https://github.com/python/mypy/issues/1424
req_success = await requirements.async_process_requirements(
hass, module_path, module.REQUIREMENTS) # type: ignore
if not req_success:
return None
processed.add(module_name)
return module

View File

@ -0,0 +1,82 @@
"""Example auth module."""
import logging
from typing import Any, Dict, Optional
import voluptuous as vol
from homeassistant.core import HomeAssistant
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
vol.Required('data'): [vol.Schema({
vol.Required('user_id'): str,
vol.Required('pin'): str,
})]
}, extra=vol.PREVENT_EXTRA)
_LOGGER = logging.getLogger(__name__)
@MULTI_FACTOR_AUTH_MODULES.register('insecure_example')
class InsecureExampleModule(MultiFactorAuthModule):
"""Example auth module validate pin."""
DEFAULT_TITLE = 'Insecure Personal Identify Number'
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._data = config['data']
@property
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({'pin': str})
@property
def setup_schema(self) -> Optional[vol.Schema]:
"""Validate async_setup_user input data."""
return vol.Schema({'pin': str})
async def async_setup_user(self, user_id: str, setup_data: Any) -> None:
"""Set up user to use mfa module."""
# data shall has been validate in caller
pin = setup_data['pin']
for data in self._data:
if data['user_id'] == user_id:
# already setup, override
data['pin'] = pin
return
self._data.append({'user_id': user_id, 'pin': pin})
async def async_depose_user(self, user_id: str) -> None:
"""Remove user from mfa module."""
found = None
for data in self._data:
if data['user_id'] == user_id:
found = data
break
if found:
self._data.remove(found)
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
for data in self._data:
if data['user_id'] == user_id:
return True
return False
async def async_validation(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
for data in self._data:
if data['user_id'] == user_id:
# user_input has been validate in caller
if data['pin'] == user_input['pin']:
return True
return False

View File

@ -1,5 +1,6 @@
"""Auth models."""
from datetime import datetime, timedelta
from typing import Dict, List, NamedTuple, Optional # noqa: F401
import uuid
import attr
@ -14,17 +15,21 @@ from .util import generate_secret
class User:
"""A user."""
name = attr.ib(type=str)
name = attr.ib(type=str) # type: Optional[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)
credentials = attr.ib(
type=list, default=attr.Factory(list), cmp=False
) # type: List[Credentials]
# Tokens associated with a user.
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
refresh_tokens = attr.ib(
type=dict, default=attr.Factory(dict), cmp=False
) # type: Dict[str, RefreshToken]
@attr.s(slots=True)
@ -32,7 +37,7 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens."""
user = attr.ib(type=User)
client_id = attr.ib(type=str)
client_id = attr.ib(type=str) # type: Optional[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,
@ -48,10 +53,14 @@ class Credentials:
"""Credentials for a user on an auth provider."""
auth_provider_type = attr.ib(type=str)
auth_provider_id = attr.ib(type=str)
auth_provider_id = attr.ib(type=str) # type: Optional[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)
UserMeta = NamedTuple("UserMeta",
[('name', Optional[str]), ('is_active', bool)])

View File

@ -1,16 +1,21 @@
"""Auth providers for Home Assistant."""
import importlib
import logging
import types
from typing import Any, Dict, List, Optional
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 import data_entry_flow, requirements
from homeassistant.core import callback, HomeAssistant
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry
from homeassistant.auth.models import Credentials
from ..auth_store import AuthStore
from ..models import Credentials, User, UserMeta # noqa: F401
from ..mfa_modules import SESSION_EXPIRATION
_LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed'
@ -25,7 +30,87 @@ AUTH_PROVIDER_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
async def auth_provider_from_config(hass, store, config):
class AuthProvider:
"""Provider of user authentication."""
DEFAULT_TITLE = 'Unnamed auth provider'
def __init__(self, hass: HomeAssistant, store: AuthStore,
config: Dict[str, Any]) -> None:
"""Initialize an auth provider."""
self.hass = hass
self.store = store
self.config = config
@property
def id(self) -> Optional[str]: # pylint: disable=invalid-name
"""Return id of the auth provider.
Optional, can be None.
"""
return self.config.get(CONF_ID)
@property
def type(self) -> str:
"""Return type of the provider."""
return self.config[CONF_TYPE] # type: ignore
@property
def name(self) -> str:
"""Return the name of the auth provider."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
@property
def support_mfa(self) -> bool:
"""Return whether multi-factor auth supported by the auth provider."""
return True
async def async_credentials(self) -> List[Credentials]:
"""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: Dict[str, str]) -> Credentials:
"""Create credentials."""
return Credentials(
auth_provider_type=self.type,
auth_provider_id=self.id,
data=data,
)
# Implement by extending class
async def async_login_flow(self, context: Optional[Dict]) -> 'LoginFlow':
"""Return the data flow for logging in with auth provider.
Auth provider should extend LoginFlow and return an instance.
"""
raise NotImplementedError
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result."""
raise NotImplementedError
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
raise NotImplementedError
async def auth_provider_from_config(
hass: HomeAssistant, store: AuthStore,
config: Dict[str, Any]) -> Optional[AuthProvider]:
"""Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE]
module = await load_auth_provider_module(hass, provider_name)
@ -34,16 +119,17 @@ async def auth_provider_from_config(hass, store, config):
return None
try:
config = module.CONFIG_SCHEMA(config)
config = module.CONFIG_SCHEMA(config) # type: ignore
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)
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
async def load_auth_provider_module(hass, provider):
async def load_auth_provider_module(
hass: HomeAssistant, provider: str) -> Optional[types.ModuleType]:
"""Load an auth provider."""
try:
module = importlib.import_module(
@ -62,8 +148,10 @@ async def load_auth_provider_module(hass, provider):
elif provider in processed:
return module
# https://github.com/python/mypy/issues/1424
reqs = module.REQUIREMENTS # type: ignore
req_success = await requirements.async_process_requirements(
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
hass, 'auth provider {}'.format(provider), reqs)
if not req_success:
return None
@ -72,72 +160,88 @@ async def load_auth_provider_module(hass, provider):
return module
class AuthProvider:
"""Provider of user authentication."""
class LoginFlow(data_entry_flow.FlowHandler):
"""Handler for the login flow."""
DEFAULT_TITLE = 'Unnamed auth provider'
def __init__(self, auth_provider: AuthProvider) -> None:
"""Initialize the login flow."""
self._auth_provider = auth_provider
self._auth_module_id = None # type: Optional[str]
self._auth_manager = auth_provider.hass.auth # type: ignore
self.available_mfa_modules = [] # type: List
self.created_at = dt_util.utcnow()
self.user = None # type: Optional[User]
def __init__(self, hass, store, config):
"""Initialize an auth provider."""
self.hass = hass
self.store = store
self.config = config
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the first step of login flow.
@property
def id(self): # pylint: disable=invalid-name
"""Return id of the auth provider.
Optional, can be None.
Return self.async_show_form(step_id='init') if user_input == None.
Return await self.async_finish(flow_result) if login init step pass.
"""
return self.config.get(CONF_ID)
raise NotImplementedError
@property
def type(self):
"""Return type of the provider."""
return self.config[CONF_TYPE]
async def async_step_select_mfa_module(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of select mfa module."""
errors = {}
@property
def name(self):
"""Return the name of the auth provider."""
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
if user_input is not None:
auth_module = user_input.get('multi_factor_auth_module')
if auth_module in self.available_mfa_modules:
self._auth_module_id = auth_module
return await self.async_step_mfa()
errors['base'] = 'invalid_auth_module'
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)
]
if len(self.available_mfa_modules) == 1:
self._auth_module_id = self.available_mfa_modules[0]
return await self.async_step_mfa()
@callback
def async_create_credentials(self, data):
"""Create credentials."""
return Credentials(
auth_provider_type=self.type,
auth_provider_id=self.id,
data=data,
return self.async_show_form(
step_id='select_mfa_module',
data_schema=vol.Schema({
'multi_factor_auth_module': vol.In(self.available_mfa_modules)
}),
errors=errors,
)
# Implement by extending class
async def async_step_mfa(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of mfa validation."""
errors = {}
async def async_credential_flow(self, context):
"""Return the data flow for logging in with auth provider."""
raise NotImplementedError
auth_module = self._auth_manager.get_auth_mfa_module(
self._auth_module_id)
if auth_module is None:
# Given an invalid input to async_step_select_mfa_module
# will show invalid_auth_module error
return await self.async_step_select_mfa_module(user_input={})
async def async_get_or_create_credentials(self, flow_result):
"""Get credentials based on the flow result."""
raise NotImplementedError
if user_input is not None:
expires = self.created_at + SESSION_EXPIRATION
if dt_util.utcnow() > expires:
errors['base'] = 'login_expired'
else:
result = await auth_module.async_validation(
self.user.id, user_input) # type: ignore
if not result:
errors['base'] = 'invalid_auth'
async def async_user_meta_for_credentials(self, credentials):
"""Return extra user metadata for credentials.
if not errors:
return await self.async_finish(self.user)
Will be used to populate info when creating a new user.
return self.async_show_form(
step_id='mfa',
data_schema=auth_module.input_schema,
errors=errors,
)
Values to populate:
- name: string
- is_active: boolean
"""
return {}
async def async_finish(self, flow_result: Any) -> Dict:
"""Handle the pass of login flow."""
return self.async_create_entry(
title=self._auth_provider.name,
data=flow_result
)

View File

@ -3,24 +3,24 @@ import base64
from collections import OrderedDict
import hashlib
import hmac
from typing import Dict # noqa: F401 pylint: disable=unused-import
from typing import Any, Dict, List, Optional, cast
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.const import CONF_ID
from homeassistant.core import callback
from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.auth.util import generate_secret
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
from ..util import generate_secret
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_provider.homeassistant'
def _disallow_id(conf):
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
"""Disallow ID in config."""
if CONF_ID in conf:
raise vol.Invalid(
@ -46,13 +46,13 @@ class InvalidUser(HomeAssistantError):
class Data:
"""Hold the user data."""
def __init__(self, hass):
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the user data store."""
self.hass = hass
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._data = None
self._data = None # type: Optional[Dict[str, Any]]
async def async_load(self):
async def async_load(self) -> None:
"""Load stored data."""
data = await self._store.async_load()
@ -65,9 +65,9 @@ class Data:
self._data = data
@property
def users(self):
def users(self) -> List[Dict[str, str]]:
"""Return users."""
return self._data['users']
return self._data['users'] # type: ignore
def validate_login(self, username: str, password: str) -> None:
"""Validate a username and password.
@ -79,7 +79,7 @@ class Data:
found = None
# Compare all users to avoid timing attacks.
for user in self._data['users']:
for user in self.users:
if username == user['username']:
found = user
@ -94,8 +94,8 @@ class Data:
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password."""
hashed = hashlib.pbkdf2_hmac(
'sha512', password.encode(), self._data['salt'].encode(), 100000)
salt = self._data['salt'].encode() # type: ignore
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
if for_storage:
hashed = base64.b64encode(hashed)
return hashed
@ -137,7 +137,7 @@ class Data:
else:
raise InvalidUser
async def async_save(self):
async def async_save(self) -> None:
"""Save data."""
await self._store.async_save(self._data)
@ -150,7 +150,7 @@ class HassAuthProvider(AuthProvider):
data = None
async def async_initialize(self):
async def async_initialize(self) -> None:
"""Initialize the auth provider."""
if self.data is not None:
return
@ -158,19 +158,22 @@ class HassAuthProvider(AuthProvider):
self.data = Data(self.hass)
await self.data.async_load()
async def async_credential_flow(self, context):
async def async_login_flow(
self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return LoginFlow(self)
return HassLoginFlow(self)
async def async_validate_login(self, username: str, password: str):
"""Helper to validate a username and password."""
async def async_validate_login(self, username: str, password: str) -> None:
"""Validate a username and password."""
if self.data is None:
await self.async_initialize()
assert self.data is not None
await self.hass.async_add_executor_job(
self.data.validate_login, username, password)
async def async_get_or_create_credentials(self, flow_result):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result['username']
@ -183,17 +186,17 @@ class HassAuthProvider(AuthProvider):
'username': username
})
async def async_user_meta_for_credentials(self, credentials):
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""Get extra info for this credential."""
return {
'name': credentials.data['username'],
'is_active': True,
}
return UserMeta(name=credentials.data['username'], is_active=True)
async def async_will_remove_credentials(self, credentials):
async def async_will_remove_credentials(
self, credentials: Credentials) -> None:
"""When credentials get removed, also remove the auth."""
if self.data is None:
await self.async_initialize()
assert self.data is not None
try:
self.data.async_remove_auth(credentials.data['username'])
@ -203,29 +206,26 @@ class HassAuthProvider(AuthProvider):
pass
class LoginFlow(data_entry_flow.FlowHandler):
class HassLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
await self._auth_provider.async_validate_login(
user_input['username'], user_input['password'])
await cast(HassAuthProvider, self._auth_provider)\
.async_validate_login(user_input['username'],
user_input['password'])
except InvalidAuth:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
user_input.pop('password')
return await self.async_finish(user_input)
schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str

View File

@ -1,14 +1,15 @@
"""Example auth provider."""
from collections import OrderedDict
import hmac
from typing import Any, Dict, Optional, cast
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import data_entry_flow
from homeassistant.core import callback
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
USER_SCHEMA = vol.Schema({
@ -31,13 +32,13 @@ class InvalidAuthError(HomeAssistantError):
class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords."""
async def async_credential_flow(self, context):
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return LoginFlow(self)
return ExampleLoginFlow(self)
@callback
def async_validate_login(self, username, password):
"""Helper to validate a username and password."""
def async_validate_login(self, username: str, password: str) -> None:
"""Validate a username and password."""
user = None
# Compare all users to avoid timing attacks.
@ -56,7 +57,8 @@ class ExampleAuthProvider(AuthProvider):
password.encode('utf-8')):
raise InvalidAuthError
async def async_get_or_create_credentials(self, flow_result):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result['username']
@ -69,49 +71,45 @@ class ExampleAuthProvider(AuthProvider):
'username': username
})
async def async_user_meta_for_credentials(self, credentials):
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""Return extra user metadata for credentials.
Will be used to populate info when creating a new user.
"""
username = credentials.data['username']
info = {
'is_active': True,
}
name = None
for user in self.config['users']:
if user['username'] == username:
info['name'] = user.get('name')
name = user.get('name')
break
return info
return UserMeta(name=name, is_active=True)
class LoginFlow(data_entry_flow.FlowHandler):
class ExampleLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
self._auth_provider.async_validate_login(
user_input['username'], user_input['password'])
cast(ExampleAuthProvider, self._auth_provider)\
.async_validate_login(user_input['username'],
user_input['password'])
except InvalidAuthError:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
user_input.pop('password')
return await self.async_finish(user_input)
schema = OrderedDict()
schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str
schema['password'] = str

View File

@ -3,16 +3,17 @@ Support Legacy API password auth provider.
It will be removed when auth system production ready
"""
from collections import OrderedDict
import hmac
from typing import Any, Dict, Optional, cast
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant import data_entry_flow
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
USER_SCHEMA = vol.Schema({
@ -36,25 +37,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
DEFAULT_TITLE = 'Legacy API Password'
async def async_credential_flow(self, context):
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return LoginFlow(self)
return LegacyLoginFlow(self)
@callback
def async_validate_login(self, password):
"""Helper to validate a username and password."""
if not hasattr(self.hass, 'http'):
raise ValueError('http component is not loaded')
def async_validate_login(self, password: str) -> None:
"""Validate a username and password."""
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
if self.hass.http.api_password is None:
raise ValueError('http component is not configured using'
' api_password')
if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'),
if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
password.encode('utf-8')):
raise InvalidAuthError
async def async_get_or_create_credentials(self, flow_result):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Return LEGACY_USER always."""
for credential in await self.async_credentials():
if credential.data['username'] == LEGACY_USER:
@ -64,47 +61,43 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
'username': LEGACY_USER
})
async def async_user_meta_for_credentials(self, credentials):
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""
Set name as LEGACY_USER always.
Will be used to populate info when creating a new user.
"""
return {
'name': LEGACY_USER,
'is_active': True,
}
return UserMeta(name=LEGACY_USER, is_active=True)
class LoginFlow(data_entry_flow.FlowHandler):
class LegacyLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider):
"""Initialize the login flow."""
self._auth_provider = auth_provider
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
hass_http = getattr(self.hass, 'http', None)
if hass_http is None or not hass_http.api_password:
return self.async_abort(
reason='no_api_password_set'
)
if user_input is not None:
try:
self._auth_provider.async_validate_login(
user_input['password'])
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
.async_validate_login(user_input['password'])
except InvalidAuthError:
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data={}
)
schema = OrderedDict()
schema['password'] = str
return await self.async_finish({})
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
data_schema=vol.Schema({'password': str}),
errors=errors,
)

View File

@ -3,12 +3,16 @@
It shows list of users if access from trusted network.
Abort login flow if not access from trusted network.
"""
from typing import Any, Dict, Optional, cast
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA)
@ -31,16 +35,24 @@ class TrustedNetworksAuthProvider(AuthProvider):
DEFAULT_TITLE = 'Trusted Networks'
async def async_credential_flow(self, context):
@property
def support_mfa(self) -> bool:
"""Trusted Networks auth provider does not support MFA."""
return False
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
assert context is not None
users = await self.store.async_get_users()
available_users = {user.id: user.name
for user in users
if not user.system_generated and user.is_active}
return LoginFlow(self, context.get('ip_address'), available_users)
return TrustedNetworksLoginFlow(
self, cast(str, context.get('ip_address')), available_users)
async def async_get_or_create_credentials(self, flow_result):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result."""
user_id = flow_result['user']
@ -59,7 +71,8 @@ class TrustedNetworksAuthProvider(AuthProvider):
# We only allow login as exist user
raise InvalidUserError
async def async_user_meta_for_credentials(self, credentials):
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
"""Return extra user metadata for credentials.
Trusted network auth provider should never create new user.
@ -67,35 +80,41 @@ class TrustedNetworksAuthProvider(AuthProvider):
raise NotImplementedError
@callback
def async_validate_access(self, ip_address):
def async_validate_access(self, ip_address: str) -> None:
"""Make sure the access from trusted networks.
Raise InvalidAuthError if not.
Raise InvalidAuthError if trusted_networks is not config
Raise InvalidAuthError if trusted_networks is not configured.
"""
if (not hasattr(self.hass, 'http') or
not self.hass.http or not self.hass.http.trusted_networks):
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
if not hass_http or not hass_http.trusted_networks:
raise InvalidAuthError('trusted_networks is not configured')
if not any(ip_address in trusted_network for trusted_network
in self.hass.http.trusted_networks):
in hass_http.trusted_networks):
raise InvalidAuthError('Not in trusted_networks')
class LoginFlow(data_entry_flow.FlowHandler):
class TrustedNetworksLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider, ip_address, available_users):
def __init__(self, auth_provider: TrustedNetworksAuthProvider,
ip_address: str, available_users: Dict[str, Optional[str]]) \
-> None:
"""Initialize the login flow."""
self._auth_provider = auth_provider
super().__init__(auth_provider)
self._available_users = available_users
self._ip_address = ip_address
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
try:
self._auth_provider.async_validate_access(self._ip_address)
cast(TrustedNetworksAuthProvider, self._auth_provider)\
.async_validate_access(self._ip_address)
except InvalidAuthError:
errors['base'] = 'invalid_auth'
@ -111,10 +130,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
errors['base'] = 'invalid_auth'
if not errors:
return self.async_create_entry(
title=self._auth_provider.name,
data=user_input
)
return await self.async_finish(user_input)
schema = {'user': vol.In(self._available_users)}

View File

@ -87,9 +87,11 @@ async def async_from_config_dict(config: Dict[str, Any],
log_no_color)
core_config = config.get(core.DOMAIN, {})
has_api_password = bool((config.get('http') or {}).get('api_password'))
try:
await conf_util.async_process_ha_core_config(hass, core_config)
await conf_util.async_process_ha_core_config(
hass, core_config, has_api_password)
except vol.Invalid as ex:
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None
@ -307,7 +309,7 @@ def async_enable_logging(hass: core.HomeAssistant,
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error(
"Unable to setup error log %s (access denied)", err_log_path)
"Unable to set up error log %s (access denied)", err_log_path)
async def async_mount_local_lib_path(config_dir: str) -> str:

View File

@ -141,7 +141,7 @@ def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)

View File

@ -1,32 +1,31 @@
"""
Support for HomematicIP alarm control panel.
Support for HomematicIP Cloud alarm control panel.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/
"""
import logging
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.components.homematicip_cloud import (
HMIPC_HAPID, HomematicipGenericDevice)
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
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__)
DEPENDENCIES = ['homematicip_cloud']
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."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the HomematicIP Cloud alarm control devices."""
pass
@ -45,7 +44,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
"""Representation of an HomematicIP security zone group."""
"""Representation of an HomematicIP Cloud security zone group."""
def __init__(self, home, device):
"""Initialize the security zone group."""

View File

@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try:
simplisafe = SimpliSafeApiInterface(username, password)
except SimpliSafeAPIException:
_LOGGER.error("Failed to setup SimpliSafe")
_LOGGER.error("Failed to set up SimpliSafe")
return
systems = []

View File

@ -111,6 +111,7 @@ def async_setup(hass, config):
for alert_id in alert_ids:
alert = all_alerts[alert_id]
alert.async_set_context(service_call.context)
if service_call.service == SERVICE_TURN_ON:
yield from alert.async_turn_on()
elif service_call.service == SERVICE_TOGGLE:

View File

@ -13,12 +13,13 @@ import homeassistant.util.color as color_util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.util.decorator import Registry
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
@ -53,6 +54,7 @@ CONF_DISPLAY_CATEGORIES = 'display_categories'
HANDLERS = Registry()
ENTITY_ADAPTERS = Registry()
EVENT_ALEXA_SMART_HOME = 'alexa_smart_home'
class _DisplayCategory:
@ -159,7 +161,8 @@ class _AlexaEntity:
The API handlers should manipulate entities only through this interface.
"""
def __init__(self, config, entity):
def __init__(self, hass, config, entity):
self.hass = hass
self.config = config
self.entity = entity
self.entity_conf = config.entity_config.get(entity.entity_id, {})
@ -383,6 +386,10 @@ class _AlexaInputController(_AlexaInterface):
class _AlexaTemperatureSensor(_AlexaInterface):
def __init__(self, hass, entity):
_AlexaInterface.__init__(self, entity)
self.hass = hass
def name(self):
return 'Alexa.TemperatureSensor'
@ -396,9 +403,10 @@ class _AlexaTemperatureSensor(_AlexaInterface):
if name != 'temperature':
raise _UnsupportedProperty(name)
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
temp = self.entity.state
if self.entity.domain == climate.DOMAIN:
unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(
climate.ATTR_CURRENT_TEMPERATURE)
return {
@ -408,6 +416,10 @@ class _AlexaTemperatureSensor(_AlexaInterface):
class _AlexaThermostatController(_AlexaInterface):
def __init__(self, hass, entity):
_AlexaInterface.__init__(self, entity)
self.hass = hass
def name(self):
return 'Alexa.ThermostatController'
@ -438,8 +450,7 @@ class _AlexaThermostatController(_AlexaInterface):
raise _UnsupportedProperty(name)
return mode
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = None
unit = self.hass.config.units.temperature_unit
if name == 'targetSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TEMPERATURE)
elif name == 'lowerSetpoint':
@ -490,8 +501,8 @@ class _ClimateCapabilities(_AlexaEntity):
return [_DisplayCategory.THERMOSTAT]
def interfaces(self):
yield _AlexaThermostatController(self.entity)
yield _AlexaTemperatureSensor(self.entity)
yield _AlexaThermostatController(self.hass, self.entity)
yield _AlexaTemperatureSensor(self.hass, self.entity)
@ENTITY_ADAPTERS.register(cover.DOMAIN)
@ -608,11 +619,11 @@ class _SensorCapabilities(_AlexaEntity):
def interfaces(self):
attrs = self.entity.attributes
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (
TEMP_FAHRENHEIT,
TEMP_CELSIUS,
):
yield _AlexaTemperatureSensor(self.entity)
yield _AlexaTemperatureSensor(self.hass, self.entity)
class _Cause:
@ -703,24 +714,47 @@ class SmartHomeView(http.HomeAssistantView):
return b'' if response is None else self.json(response)
@asyncio.coroutine
def async_handle_message(hass, config, message):
async def async_handle_message(hass, config, request, context=None):
"""Handle incoming API messages."""
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
assert request[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
if context is None:
context = ha.Context()
# Read head data
message = message[API_DIRECTIVE]
namespace = message[API_HEADER]['namespace']
name = message[API_HEADER]['name']
request = request[API_DIRECTIVE]
namespace = request[API_HEADER]['namespace']
name = request[API_HEADER]['name']
# Do we support this API request?
funct_ref = HANDLERS.get((namespace, name))
if not funct_ref:
if funct_ref:
response = await funct_ref(hass, config, request, context)
else:
_LOGGER.warning(
"Unsupported API request %s/%s", namespace, name)
return api_error(message)
response = api_error(request)
return (yield from funct_ref(hass, config, message))
request_info = {
'namespace': namespace,
'name': name,
}
if API_ENDPOINT in request and 'endpointId' in request[API_ENDPOINT]:
request_info['entity_id'] = \
request[API_ENDPOINT]['endpointId'].replace('#', '.')
response_header = response[API_EVENT][API_HEADER]
hass.bus.async_fire(EVENT_ALEXA_SMART_HOME, {
'request': request_info,
'response': {
'namespace': response_header['namespace'],
'name': response_header['name'],
}
}, context=context)
return response
def api_message(request,
@ -784,8 +818,7 @@ def api_error(request,
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
@asyncio.coroutine
def async_api_discovery(hass, config, request):
async def async_api_discovery(hass, config, request, context):
"""Create a API formatted discovery response.
Async friendly.
@ -800,7 +833,7 @@ def async_api_discovery(hass, config, request):
if entity.domain not in ENTITY_ADAPTERS:
continue
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
alexa_entity = ENTITY_ADAPTERS[entity.domain](hass, config, entity)
endpoint = {
'displayCategories': alexa_entity.display_categories(),
@ -827,8 +860,7 @@ def async_api_discovery(hass, config, request):
def extract_entity(funct):
"""Decorate for extract entity object from request."""
@asyncio.coroutine
def async_api_entity_wrapper(hass, config, request):
async def async_api_entity_wrapper(hass, config, request, context):
"""Process a turn on request."""
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
@ -839,15 +871,14 @@ def extract_entity(funct):
request[API_HEADER]['name'], entity_id)
return api_error(request, error_type='NO_SUCH_ENDPOINT')
return (yield from funct(hass, config, request, entity))
return await funct(hass, config, request, context, entity)
return async_api_entity_wrapper
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
@extract_entity
@asyncio.coroutine
def async_api_turn_on(hass, config, request, entity):
async def async_api_turn_on(hass, config, request, context, entity):
"""Process a turn on request."""
domain = entity.domain
if entity.domain == group.DOMAIN:
@ -857,17 +888,16 @@ def async_api_turn_on(hass, config, request, entity):
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_OPEN_COVER
yield from hass.services.async_call(domain, service, {
await hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
@extract_entity
@asyncio.coroutine
def async_api_turn_off(hass, config, request, entity):
async def async_api_turn_off(hass, config, request, context, entity):
"""Process a turn off request."""
domain = entity.domain
if entity.domain == group.DOMAIN:
@ -877,32 +907,30 @@ def async_api_turn_off(hass, config, request, entity):
if entity.domain == cover.DOMAIN:
service = cover.SERVICE_CLOSE_COVER
yield from hass.services.async_call(domain, service, {
await hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
@extract_entity
@asyncio.coroutine
def async_api_set_brightness(hass, config, request, entity):
async def async_api_set_brightness(hass, config, request, context, entity):
"""Process a set brightness request."""
brightness = int(request[API_PAYLOAD]['brightness'])
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_brightness(hass, config, request, entity):
async def async_api_adjust_brightness(hass, config, request, context, entity):
"""Process an adjust brightness request."""
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
@ -915,18 +943,17 @@ def async_api_adjust_brightness(hass, config, request, entity):
# set brightness
brightness = max(0, brightness_delta + current)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
@extract_entity
@asyncio.coroutine
def async_api_set_color(hass, config, request, entity):
async def async_api_set_color(hass, config, request, context, entity):
"""Process a set color request."""
rgb = color_util.color_hsb_to_RGB(
float(request[API_PAYLOAD]['color']['hue']),
@ -934,25 +961,25 @@ def async_api_set_color(hass, config, request, entity):
float(request[API_PAYLOAD]['color']['brightness'])
)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_set_color_temperature(hass, config, request, entity):
async def async_api_set_color_temperature(hass, config, request, context,
entity):
"""Process a set color temperature request."""
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_KELVIN: kelvin,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@ -960,17 +987,17 @@ def async_api_set_color_temperature(hass, config, request, entity):
@HANDLERS.register(
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_decrease_color_temp(hass, config, request, entity):
async def async_api_decrease_color_temp(hass, config, request, context,
entity):
"""Process a decrease color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
value = min(max_mireds, current + 50)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@ -978,31 +1005,30 @@ def async_api_decrease_color_temp(hass, config, request, entity):
@HANDLERS.register(
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_increase_color_temp(hass, config, request, entity):
async def async_api_increase_color_temp(hass, config, request, context,
entity):
"""Process an increase color temperature request."""
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
value = max(min_mireds, current - 50)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
@extract_entity
@asyncio.coroutine
def async_api_activate(hass, config, request, entity):
async def async_api_activate(hass, config, request, context, entity):
"""Process an activate request."""
domain = entity.domain
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
await hass.services.async_call(domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
payload = {
'cause': {'type': _Cause.VOICE_INTERACTION},
@ -1019,14 +1045,13 @@ def async_api_activate(hass, config, request, entity):
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
@extract_entity
@asyncio.coroutine
def async_api_deactivate(hass, config, request, entity):
async def async_api_deactivate(hass, config, request, context, entity):
"""Process a deactivate request."""
domain = entity.domain
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
await hass.services.async_call(domain, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
payload = {
'cause': {'type': _Cause.VOICE_INTERACTION},
@ -1043,8 +1068,7 @@ def async_api_deactivate(hass, config, request, entity):
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_set_percentage(hass, config, request, entity):
async def async_api_set_percentage(hass, config, request, context, entity):
"""Process a set percentage request."""
percentage = int(request[API_PAYLOAD]['percentage'])
service = None
@ -1066,16 +1090,15 @@ def async_api_set_percentage(hass, config, request, entity):
service = SERVICE_SET_COVER_POSITION
data[cover.ATTR_POSITION] = percentage
yield from hass.services.async_call(
entity.domain, service, data, blocking=False)
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_percentage(hass, config, request, entity):
async def async_api_adjust_percentage(hass, config, request, context, entity):
"""Process an adjust percentage request."""
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
service = None
@ -1114,20 +1137,19 @@ def async_api_adjust_percentage(hass, config, request, entity):
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
yield from hass.services.async_call(
entity.domain, service, data, blocking=False)
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.LockController', 'Lock'))
@extract_entity
@asyncio.coroutine
def async_api_lock(hass, config, request, entity):
async def async_api_lock(hass, config, request, context, entity):
"""Process a lock request."""
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
await hass.services.async_call(entity.domain, SERVICE_LOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
# Alexa expects a lockState in the response, we don't know the actual
# lockState at this point but assume it is locked. It is reported
@ -1144,20 +1166,18 @@ def async_api_lock(hass, config, request, entity):
# Not supported by Alexa yet
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
@extract_entity
@asyncio.coroutine
def async_api_unlock(hass, config, request, entity):
async def async_api_unlock(hass, config, request, context, entity):
"""Process an unlock request."""
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
await hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
}, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
@extract_entity
@asyncio.coroutine
def async_api_set_volume(hass, config, request, entity):
async def async_api_set_volume(hass, config, request, context, entity):
"""Process a set volume request."""
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
@ -1166,17 +1186,16 @@ def async_api_set_volume(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_SET,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
@extract_entity
@asyncio.coroutine
def async_api_select_input(hass, config, request, entity):
async def async_api_select_input(hass, config, request, context, entity):
"""Process a set input request."""
media_input = request[API_PAYLOAD]['input']
@ -1200,17 +1219,16 @@ def async_api_select_input(hass, config, request, entity):
media_player.ATTR_INPUT_SOURCE: media_input,
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_SELECT_SOURCE,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_volume(hass, config, request, entity):
async def async_api_adjust_volume(hass, config, request, context, entity):
"""Process an adjust volume request."""
volume_delta = int(request[API_PAYLOAD]['volume'])
@ -1229,17 +1247,16 @@ def async_api_adjust_volume(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_SET,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_volume_step(hass, config, request, entity):
async def async_api_adjust_volume_step(hass, config, request, context, entity):
"""Process an adjust volume step request."""
# media_player volume up/down service does not support specifying steps
# each component handles it differently e.g. via config.
@ -1252,13 +1269,13 @@ def async_api_adjust_volume_step(hass, config, request, entity):
}
if volume_step > 0:
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_UP,
data, blocking=False)
data, blocking=False, context=context)
elif volume_step < 0:
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_DOWN,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@ -1266,8 +1283,7 @@ def async_api_adjust_volume_step(hass, config, request, entity):
@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
@extract_entity
@asyncio.coroutine
def async_api_set_mute(hass, config, request, entity):
async def async_api_set_mute(hass, config, request, context, entity):
"""Process a set mute request."""
mute = bool(request[API_PAYLOAD]['mute'])
@ -1276,98 +1292,94 @@ def async_api_set_mute(hass, config, request, entity):
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, media_player.SERVICE_VOLUME_MUTE,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
@extract_entity
@asyncio.coroutine
def async_api_play(hass, config, request, entity):
async def async_api_play(hass, config, request, context, entity):
"""Process a play request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PLAY,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
@extract_entity
@asyncio.coroutine
def async_api_pause(hass, config, request, entity):
async def async_api_pause(hass, config, request, context, entity):
"""Process a pause request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PAUSE,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
@extract_entity
@asyncio.coroutine
def async_api_stop(hass, config, request, entity):
async def async_api_stop(hass, config, request, context, entity):
"""Process a stop request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
@extract_entity
@asyncio.coroutine
def async_api_next(hass, config, request, entity):
async def async_api_next(hass, config, request, context, entity):
"""Process a next request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
@extract_entity
@asyncio.coroutine
def async_api_previous(hass, config, request, entity):
async def async_api_previous(hass, config, request, context, entity):
"""Process a previous request."""
data = {
ATTR_ENTITY_ID: entity.entity_id
}
yield from hass.services.async_call(
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
data, blocking=False)
data, blocking=False, context=context)
return api_message(request)
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
def api_error_temp_range(hass, request, temp, min_temp, max_temp):
"""Create temperature value out of range API error response.
Async friendly.
"""
unit = hass.config.units.temperature_unit
temp_range = {
'minimumValue': {
'value': min_temp,
@ -1388,8 +1400,9 @@ def api_error_temp_range(request, temp, min_temp, max_temp, unit):
)
def temperature_from_object(temp_obj, to_unit, interval=False):
def temperature_from_object(hass, temp_obj, interval=False):
"""Get temperature from Temperature object in requested unit."""
to_unit = hass.config.units.temperature_unit
from_unit = TEMP_CELSIUS
temp = float(temp_obj['value'])
@ -1405,9 +1418,8 @@ def temperature_from_object(temp_obj, to_unit, interval=False):
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
@extract_entity
async def async_api_set_target_temp(hass, config, request, entity):
async def async_api_set_target_temp(hass, config, request, context, entity):
"""Process a set target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
@ -1417,48 +1429,45 @@ async def async_api_set_target_temp(hass, config, request, entity):
payload = request[API_PAYLOAD]
if 'targetSetpoint' in payload:
temp = temperature_from_object(
payload['targetSetpoint'], unit)
temp = temperature_from_object(hass, payload['targetSetpoint'])
if temp < min_temp or temp > max_temp:
return api_error_temp_range(
request, temp, min_temp, max_temp, unit)
hass, request, temp, min_temp, max_temp)
data[ATTR_TEMPERATURE] = temp
if 'lowerSetpoint' in payload:
temp_low = temperature_from_object(
payload['lowerSetpoint'], unit)
temp_low = temperature_from_object(hass, payload['lowerSetpoint'])
if temp_low < min_temp or temp_low > max_temp:
return api_error_temp_range(
request, temp_low, min_temp, max_temp, unit)
hass, request, temp_low, min_temp, max_temp)
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
if 'upperSetpoint' in payload:
temp_high = temperature_from_object(
payload['upperSetpoint'], unit)
temp_high = temperature_from_object(hass, payload['upperSetpoint'])
if temp_high < min_temp or temp_high > max_temp:
return api_error_temp_range(
request, temp_high, min_temp, max_temp, unit)
hass, request, temp_high, min_temp, max_temp)
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
context=context)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
@extract_entity
async def async_api_adjust_target_temp(hass, config, request, entity):
async def async_api_adjust_target_temp(hass, config, request, context, entity):
"""Process an adjust target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
temp_delta = temperature_from_object(
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
hass, request[API_PAYLOAD]['targetSetpointDelta'], interval=True)
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
return api_error_temp_range(
request, target_temp, min_temp, max_temp, unit)
hass, request, target_temp, min_temp, max_temp)
data = {
ATTR_ENTITY_ID: entity.entity_id,
@ -1466,14 +1475,16 @@ async def async_api_adjust_target_temp(hass, config, request, entity):
}
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
context=context)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
@extract_entity
async def async_api_set_thermostat_mode(hass, config, request, entity):
async def async_api_set_thermostat_mode(hass, config, request, context,
entity):
"""Process a set thermostat mode request."""
mode = request[API_PAYLOAD]['thermostatMode']
mode = mode if isinstance(mode, str) else mode['value']
@ -1499,17 +1510,16 @@ async def async_api_set_thermostat_mode(hass, config, request, entity):
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
blocking=False)
blocking=False, context=context)
return api_message(request)
@HANDLERS.register(('Alexa', 'ReportState'))
@extract_entity
@asyncio.coroutine
def async_api_reportstate(hass, config, request, entity):
async def async_api_reportstate(hass, config, request, context, entity):
"""Process a ReportState request."""
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
alexa_entity = ENTITY_ADAPTERS[entity.domain](hass, config, entity)
properties = []
for interface in alexa_entity.interfaces():
properties.extend(interface.serialize_properties())

View File

@ -24,7 +24,7 @@ from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
import homeassistant.remote as rem
from homeassistant.helpers.json import JSONEncoder
_LOGGER = logging.getLogger(__name__)
@ -102,7 +102,7 @@ class APIEventStream(HomeAssistantView):
if event.event_type == EVENT_HOMEASSISTANT_STOP:
data = stop_obj
else:
data = json.dumps(event, cls=rem.JSONEncoder)
data = json.dumps(event, cls=JSONEncoder)
await to_write.put(data)

View File

@ -44,13 +44,26 @@ a limited expiration.
"expires_in": 1800,
"token_type": "Bearer"
}
## Revoking a refresh token
It is also possible to revoke a refresh token and all access tokens that have
ever been granted by that refresh token. Response code will ALWAYS be 200.
{
"token": "IJKLMNOPQRST",
"action": "revoke"
}
"""
import logging
import uuid
from datetime import timedelta
from aiohttp import web
import voluptuous as vol
from homeassistant.auth.models import User, Credentials
from homeassistant.components import websocket_api
from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator
@ -68,37 +81,40 @@ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CURRENT_USER,
})
RESULT_TYPE_CREDENTIALS = 'credentials'
RESULT_TYPE_USER = 'user'
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Component to allow users to login."""
store_credentials, retrieve_credentials = _create_cred_store()
store_result, retrieve_result = _create_auth_code_store()
hass.http.register_view(GrantTokenView(retrieve_credentials))
hass.http.register_view(LinkUserView(retrieve_credentials))
hass.http.register_view(TokenView(retrieve_result))
hass.http.register_view(LinkUserView(retrieve_result))
hass.components.websocket_api.async_register_command(
WS_TYPE_CURRENT_USER, websocket_current_user,
SCHEMA_WS_CURRENT_USER
)
await login_flow.async_setup(hass, store_credentials)
await login_flow.async_setup(hass, store_result)
return True
class GrantTokenView(HomeAssistantView):
"""View to grant tokens."""
class TokenView(HomeAssistantView):
"""View to issue or revoke tokens."""
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
def __init__(self, retrieve_user):
"""Initialize the token view."""
self._retrieve_user = retrieve_user
@log_invalid_auth
async def post(self, request):
@ -108,6 +124,13 @@ class GrantTokenView(HomeAssistantView):
grant_type = data.get('grant_type')
# IndieAuth 6.3.5
# The revocation endpoint is the same as the token endpoint.
# The revocation request includes an additional parameter,
# action=revoke.
if data.get('action') == 'revoke':
return await self._async_handle_revoke_token(hass, data)
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(hass, data)
@ -118,6 +141,25 @@ class GrantTokenView(HomeAssistantView):
'error': 'unsupported_grant_type',
}, status_code=400)
async def _async_handle_revoke_token(self, hass, data):
"""Handle revoke token request."""
# OAuth 2.0 Token Revocation [RFC7009]
# 2.2 The authorization server responds with HTTP status code 200
# if the token has been revoked successfully or if the client
# submitted an invalid token.
token = data.get('token')
if token is None:
return web.Response(status=200)
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
if refresh_token is None:
return web.Response(status=200)
await hass.auth.async_remove_refresh_token(refresh_token)
return web.Response(status=200)
async def _async_handle_auth_code(self, hass, data):
"""Handle authorization code request."""
client_id = data.get('client_id')
@ -132,17 +174,19 @@ class GrantTokenView(HomeAssistantView):
if code is None:
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid code',
}, status_code=400)
credentials = self._retrieve_credentials(client_id, code)
user = self._retrieve_user(client_id, RESULT_TYPE_USER, code)
if credentials is None:
if user is None or not isinstance(user, User):
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid code',
}, status_code=400)
user = await hass.auth.async_get_or_create_user(credentials)
# refresh user
user = await hass.auth.async_get_user(user.id)
if not user.is_active:
return self.json({
@ -220,7 +264,7 @@ class LinkUserView(HomeAssistantView):
user = request['hass_user']
credentials = self._retrieve_credentials(
data['client_id'], data['code'])
data['client_id'], RESULT_TYPE_CREDENTIALS, data['code'])
if credentials is None:
return self.json_message('Invalid code', status_code=400)
@ -230,37 +274,45 @@ class LinkUserView(HomeAssistantView):
@callback
def _create_cred_store():
"""Create a credential store."""
temp_credentials = {}
def _create_auth_code_store():
"""Create an in memory store."""
temp_results = {}
@callback
def store_credentials(client_id, credentials):
"""Store credentials and return a code to retrieve it."""
def store_result(client_id, result):
"""Store flow result and return a code to retrieve it."""
if isinstance(result, User):
result_type = RESULT_TYPE_USER
elif isinstance(result, Credentials):
result_type = RESULT_TYPE_CREDENTIALS
else:
raise ValueError('result has to be either User or Credentials')
code = uuid.uuid4().hex
temp_credentials[(client_id, code)] = (dt_util.utcnow(), credentials)
temp_results[(client_id, result_type, code)] = \
(dt_util.utcnow(), result_type, result)
return code
@callback
def retrieve_credentials(client_id, code):
"""Retrieve credentials."""
key = (client_id, code)
def retrieve_result(client_id, result_type, code):
"""Retrieve flow result."""
key = (client_id, result_type, code)
if key not in temp_credentials:
if key not in temp_results:
return None
created, credentials = temp_credentials.pop(key)
created, _, result = temp_results.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 result
return None
return store_credentials, retrieve_credentials
return store_result, retrieve_result
@callback

View File

@ -4,6 +4,7 @@ from html.parser import HTMLParser
from ipaddress import ip_address, ip_network
from urllib.parse import urlparse, urljoin
import aiohttp
from aiohttp.client_exceptions import ClientError
# IP addresses of loopback interfaces
@ -76,18 +77,17 @@ async def fetch_redirect_uris(hass, url):
We do not implement extracting redirect uris from headers.
"""
session = hass.helpers.aiohttp_client.async_get_clientsession()
parser = LinkTagParser('redirect_uri')
chunks = 0
try:
resp = await session.get(url, timeout=5)
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=5) as resp:
async for data in resp.content.iter_chunked(1024):
parser.feed(data.decode())
chunks += 1
async for data in resp.content.iter_chunked(1024):
parser.feed(data.decode())
chunks += 1
if chunks == 10:
break
if chunks == 10:
break
except (asyncio.TimeoutError, ClientError):
pass

View File

@ -22,10 +22,14 @@ Pass in parameter 'client_id' and 'redirect_url' validate by indieauth.
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
are identified by type and id.
And optional parameter 'type' has to set as 'link_user' if login flow used for
link credential to exist user. Default 'type' is 'authorize'.
{
"client_id": "https://hassbian.local:8123/",
"handler": ["local_provider", null],
"redirect_url": "https://hassbian.local:8123/"
"redirect_url": "https://hassbian.local:8123/",
"type': "authorize"
}
Return value will be a step in a data entry flow. See the docs for data entry
@ -49,6 +53,9 @@ flow for details.
Progress the flow. Most flows will be 1 page, but could optionally add extra
login challenges, like TFA. Once the flow has finished, the returned step will
have type "create_entry" and "result" key will contain an authorization code.
The authorization code associated with an authorized user by default, it will
associate with an credential if "type" set to "link_user" in
"/auth/login_flow"
{
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
@ -71,12 +78,12 @@ from homeassistant.components.http.view import HomeAssistantView
from . import indieauth
async def async_setup(hass, store_credentials):
async def async_setup(hass, store_result):
"""Component to allow users to login."""
hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
hass.http.register_view(
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
LoginFlowResourceView(hass.auth.login_flow, store_result))
class AuthProvidersView(HomeAssistantView):
@ -138,6 +145,7 @@ class LoginFlowIndexView(HomeAssistantView):
vol.Required('client_id'): str,
vol.Required('handler'): vol.Any(str, list),
vol.Required('redirect_uri'): str,
vol.Optional('type', default='authorize'): str,
}))
@log_invalid_auth
async def post(self, request, data):
@ -153,7 +161,10 @@ class LoginFlowIndexView(HomeAssistantView):
try:
result = await self._flow_mgr.async_init(
handler, context={'ip_address': request[KEY_REAL_IP]})
handler, context={
'ip_address': request[KEY_REAL_IP],
'credential_only': data.get('type') == 'link_user',
})
except data_entry_flow.UnknownHandler:
return self.json_message('Invalid handler specified', 404)
except data_entry_flow.UnknownStep:
@ -169,10 +180,10 @@ class LoginFlowResourceView(HomeAssistantView):
name = 'api:auth:login_flow:resource'
requires_auth = False
def __init__(self, flow_mgr, store_credentials):
def __init__(self, flow_mgr, store_result):
"""Initialize the login flow resource view."""
self._flow_mgr = flow_mgr
self._store_credentials = store_credentials
self._store_result = store_result
async def get(self, request):
"""Do not allow getting status of a flow in progress."""
@ -212,7 +223,7 @@ class LoginFlowResourceView(HomeAssistantView):
return self.json(_prepare_result_json(result))
result.pop('data')
result['result'] = self._store_credentials(client_id, result['result'])
result['result'] = self._store_result(client_id, result['result'])
return self.json(result)

View File

@ -1,5 +1,5 @@
"""
Allow to setup simple automation rules via the config file.
Allow to set up simple automation rules via the config file.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/automation/

View File

@ -58,7 +58,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)

View File

@ -71,7 +71,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
@property
def should_poll(self) -> bool:
"""Data update is triggered from BMWConnectedDriveEntity."""
"""Return False.
Data update is triggered from BMWConnectedDriveEntity.
"""
return False
@property

View File

@ -58,7 +58,7 @@ class EgardiaBinarySensor(BinarySensorDevice):
@property
def name(self):
"""The name of the device."""
"""Return the name of the device."""
return self._name
@property
@ -74,5 +74,5 @@ class EgardiaBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""The device class."""
"""Return the device class."""
return self._device_class

View File

@ -90,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = HikvisionData(hass, url, port, name, username, password)
if data.sensors is None:
_LOGGER.error("Hikvision event stream has no data, unable to setup")
_LOGGER.error("Hikvision event stream has no data, unable to set up")
return False
entities = []

View File

@ -5,27 +5,28 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.homematic/
"""
import logging
from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematic']
SENSOR_TYPES_CLASS = {
'Remote': None,
'ShutterContact': 'opening',
'MaxShutterContact': 'opening',
'IPShutterContact': 'opening',
'Smoke': 'smoke',
'SmokeV2': 'smoke',
'MaxShutterContact': 'opening',
'Motion': 'motion',
'MotionV2': 'motion',
'RemoteMotion': None,
'WeatherSensor': None,
'TiltSensor': None,
'PresenceIP': 'motion',
'Remote': None,
'RemoteMotion': None,
'ShutterContact': 'opening',
'Smoke': 'smoke',
'SmokeV2': 'smoke',
'TiltSensor': None,
'WeatherSensor': None,
}

View File

@ -1,16 +1,15 @@
"""
Support for HomematicIP binary sensor.
Support for HomematicIP Cloud binary sensor.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.homematicip_cloud/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
HMIPC_HAPID)
HMIPC_HAPID, HomematicipGenericDevice)
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
DEPENDENCIES = ['homematicip_cloud']
@ -19,14 +18,14 @@ _LOGGER = logging.getLogger(__name__)
STATE_SMOKE_OFF = 'IDLE_OFF'
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the binary sensor devices."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the HomematicIP Cloud 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."""
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector)
@ -45,7 +44,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP shutter contact."""
"""Representation of a HomematicIP Cloud shutter contact."""
@property
def device_class(self):
@ -65,7 +64,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP motion detector."""
"""Representation of a HomematicIP Cloud motion detector."""
@property
def device_class(self):
@ -81,7 +80,7 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
"""HomematicIP smoke detector."""
"""Representation of a HomematicIP Cloud smoke detector."""
@property
def device_class(self):

View File

@ -2,15 +2,15 @@
Support for INSTEON dimmers via PowerLinc Modem.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.insteon_plm/
https://home-assistant.io/components/binary_sensor.insteon/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.insteon_plm import InsteonPLMEntity
from homeassistant.components.insteon import InsteonEntity
DEPENDENCIES = ['insteon_plm']
DEPENDENCIES = ['insteon']
_LOGGER = logging.getLogger(__name__)
@ -24,27 +24,27 @@ SENSOR_TYPES = {'openClosedSensor': 'opening',
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm'].get('plm')
"""Set up the INSTEON device class for the hass platform."""
insteon_modem = hass.data['insteon'].get('modem')
address = discovery_info['address']
device = plm.devices[address]
device = insteon_modem.devices[address]
state_key = discovery_info['state_key']
name = device.states[state_key].name
if name != 'dryLeakSensor':
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
device.address.hex, device.states[state_key].name)
new_entity = InsteonPLMBinarySensor(device, state_key)
new_entity = InsteonBinarySensor(device, state_key)
async_add_devices([new_entity])
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
"""A Class for an Insteon device entity."""
def __init__(self, device, state_key):
"""Initialize the INSTEON PLM binary sensor."""
"""Initialize the INSTEON binary sensor."""
super().__init__(device, state_key)
self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name)

View File

@ -105,7 +105,7 @@ class KNXBinarySensor(BinarySensorDevice):
def __init__(self, hass, device):
"""Initialize of KNX binary sensor."""
self.device = device
self._device = device
self.hass = hass
self.async_register_callbacks()
self.automations = []
@ -116,12 +116,12 @@ class KNXBinarySensor(BinarySensorDevice):
async def after_update_callback(device):
"""Call after device was updated."""
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
self._device.register_device_updated_cb(after_update_callback)
@property
def name(self):
"""Return the name of the KNX device."""
return self.device.name
return self._device.name
@property
def available(self):
@ -136,9 +136,9 @@ class KNXBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""Return the class of this sensor."""
return self.device.device_class
return self._device.device_class
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.device.is_on()
return self._device.is_on()

View File

@ -130,7 +130,7 @@ class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
def update(self):
"""Retrieve latest state."""
value = getattr(self.device, self.variable)
value = getattr(self._device, self.variable)
if self.variable in STRUCTURE_BINARY_TYPES:
self._state = bool(STRUCTURE_BINARY_STATE_MAP
[self.variable].get(value))
@ -154,4 +154,5 @@ class NestActivityZoneSensor(NestBinarySensor):
def update(self):
"""Retrieve latest state."""
self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id)
self._state = self._device.has_ongoing_motion_in_zone(
self.zone.zone_id)

View File

@ -31,4 +31,4 @@ class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice):
@property
def is_on(self):
"""Return true if switch is on."""
return self.device.is_on
return self._device.is_on

View File

@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.15.0']
REQUIREMENTS = ['numpy==1.15.1']
_LOGGER = logging.getLogger(__name__)

View File

@ -28,11 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
devices.append(XiaomiMotionSensor(device, hass, gateway))
elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'window_status'
devices.append(XiaomiDoorSensor(device, data_key, gateway))
devices.append(XiaomiDoorSensor(device, gateway))
elif model == 'sensor_wleak.aq1':
devices.append(XiaomiWaterLeakSensor(device, gateway))
elif model in ['smoke', 'sensor_smoke']:
@ -44,17 +40,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'channel_0'
data_key = 'button_0'
devices.append(XiaomiButton(device, 'Switch', data_key,
hass, gateway))
elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'channel_0'
else:
data_key = 'button_0'
devices.append(XiaomiButton(device, 'Wall Switch', data_key,
hass, gateway))
elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']:
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key_left = 'channel_0'
data_key_right = 'channel_1'
else:
data_key_left = 'button_0'
data_key_right = 'button_1'
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
'channel_0', hass, gateway))
data_key_left, hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
'channel_1', hass, gateway))
data_key_right, hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
'dual_channel', hass, gateway))
elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']:
@ -119,7 +125,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
if value is None:
return False
if value == '1':
if value in ('1', '2'):
if self._state:
return False
self._state = True
@ -194,9 +200,13 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
class XiaomiDoorSensor(XiaomiBinarySensor):
"""Representation of a XiaomiDoorSensor."""
def __init__(self, device, data_key, xiaomi_hub):
def __init__(self, device, xiaomi_hub):
"""Initialize the XiaomiDoorSensor."""
self._open_since = 0
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'window_status'
XiaomiBinarySensor.__init__(self, device, 'Door Window Sensor',
xiaomi_hub, data_key, 'opening')
@ -237,8 +247,12 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
def __init__(self, device, xiaomi_hub):
"""Initialize the XiaomiWaterLeakSensor."""
if 'proto' not in device or int(device['proto'][0:1]) == 1:
data_key = 'status'
else:
data_key = 'wleak_status'
XiaomiBinarySensor.__init__(self, device, 'Water Leak Sensor',
xiaomi_hub, 'status', 'moisture')
xiaomi_hub, data_key, 'moisture')
def parse_data(self, data, raw_data):
"""Parse data sent by gateway."""
@ -285,7 +299,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
if value is None:
return False
if value == '1':
if value in ('1', '2'):
if self._state:
return False
self._state = True
@ -359,6 +373,10 @@ class XiaomiCube(XiaomiBinarySensor):
self._hass = hass
self._last_action = None
self._state = False
if 'proto' not in device or int(device['proto'][0:1]) == 1:
self._data_key = 'status'
else:
self._data_key = 'cube_status'
XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub,
None, None)
@ -371,19 +389,12 @@ class XiaomiCube(XiaomiBinarySensor):
def parse_data(self, data, raw_data):
"""Parse data sent by gateway."""
if 'status' in data:
if self._data_key in data:
self._hass.bus.fire('cube_action', {
'entity_id': self.entity_id,
'action_type': data['status']
'action_type': data[self._data_key]
})
self._last_action = data['status']
if 'cube_status' in data:
self._hass.bus.fire('cube_action', {
'entity_id': self.entity_id,
'action_type': data['cube_status']
})
self._last_action = data['cube_status']
self._last_action = data[self._data_key]
if 'rotate' in data:
self._hass.bus.fire('cube_action', {

View File

@ -20,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
CONF_REGION = 'region'
CONF_READ_ONLY = 'read_only'
ATTR_VIN = 'vin'
ACCOUNT_SCHEMA = vol.Schema({
@ -27,6 +28,7 @@ ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_REGION): vol.Any('north_america', 'china',
'rest_of_world'),
vol.Optional(CONF_READ_ONLY, default=False): cv.boolean,
})
CONFIG_SCHEMA = vol.Schema({
@ -82,8 +84,10 @@ def setup_account(account_config: dict, hass, name: str) \
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
region = account_config[CONF_REGION]
read_only = account_config[CONF_READ_ONLY]
_LOGGER.debug('Adding new account %s', name)
cd_account = BMWConnectedDriveAccount(username, password, region, name)
cd_account = BMWConnectedDriveAccount(username, password, region, name,
read_only)
def execute_service(call):
"""Execute a service for a vehicle.
@ -99,13 +103,13 @@ def setup_account(account_config: dict, hass, name: str) \
function_name = _SERVICE_MAP[call.service]
function_call = getattr(vehicle.remote_services, function_name)
function_call()
# register the remote services
for service in _SERVICE_MAP:
hass.services.register(
DOMAIN, service,
execute_service,
schema=SERVICE_SCHEMA)
if not read_only:
# register the remote services
for service in _SERVICE_MAP:
hass.services.register(
DOMAIN, service,
execute_service,
schema=SERVICE_SCHEMA)
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
@ -122,13 +126,14 @@ class BMWConnectedDriveAccount:
"""Representation of a BMW vehicle."""
def __init__(self, username: str, password: str, region_str: str,
name: str) -> None:
name: str, read_only) -> None:
"""Constructor."""
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
region = get_region_from_name(region_str)
self.read_only = read_only
self.account = ConnectedDriveAccount(username, password, region)
self.name = name
self._update_listeners = []

View File

@ -145,7 +145,7 @@ async def async_get_image(hass, entity_id, timeout=10):
component = hass.data.get(DOMAIN)
if component is None:
raise HomeAssistantError('Camera component not setup')
raise HomeAssistantError('Camera component not set up')
camera = component.get_entity(entity_id)
@ -214,7 +214,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)

View File

@ -15,7 +15,7 @@ import voluptuous as vol
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, CONF_VERIFY_SSL)
from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
@ -42,6 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
@ -65,6 +66,7 @@ class GenericCamera(Camera):
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self._frame_interval = 1 / device_info[CONF_FRAMERATE]
self.content_type = device_info[CONF_CONTENT_TYPE]
self.verify_ssl = device_info[CONF_VERIFY_SSL]
username = device_info.get(CONF_USERNAME)
password = device_info.get(CONF_PASSWORD)
@ -108,7 +110,8 @@ class GenericCamera(Camera):
def fetch():
"""Read image from a URL."""
try:
response = requests.get(url, timeout=10, auth=self._auth)
response = requests.get(url, timeout=10, auth=self._auth,
verify=self.verify_ssl)
return response.content
except requests.exceptions.RequestException as error:
_LOGGER.error("Error getting camera image: %s", error)
@ -119,7 +122,8 @@ class GenericCamera(Camera):
# async
else:
try:
websession = async_get_clientsession(self.hass)
websession = async_get_clientsession(
self.hass, verify_ssl=self.verify_ssl)
with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from websession.get(
url, auth=self._auth)

View File

@ -46,7 +46,7 @@ class NestCamera(Camera):
"""Initialize a Nest Camera."""
super(NestCamera, self).__init__()
self.structure = structure
self.device = device
self._device = device
self._location = None
self._name = None
self._online = None
@ -93,7 +93,7 @@ class NestCamera(Camera):
# Calling Nest API in is_streaming setter.
# device.is_streaming would not immediately change until the process
# finished in Nest Cam.
self.device.is_streaming = False
self._device.is_streaming = False
def turn_on(self):
"""Turn on camera."""
@ -105,15 +105,15 @@ class NestCamera(Camera):
# Calling Nest API in is_streaming setter.
# device.is_streaming would not immediately change until the process
# finished in Nest Cam.
self.device.is_streaming = True
self._device.is_streaming = True
def update(self):
"""Cache value from Python-nest."""
self._location = self.device.where
self._name = self.device.name
self._online = self.device.online
self._is_streaming = self.device.is_streaming
self._is_video_history_enabled = self.device.is_video_history_enabled
self._location = self._device.where
self._name = self._device.name
self._online = self._device.online
self._is_streaming = self._device.is_streaming
self._is_video_history_enabled = self._device.is_video_history_enabled
if self._is_video_history_enabled:
# NestAware allowed 10/min
@ -130,7 +130,7 @@ class NestCamera(Camera):
"""Return a still image response from the camera."""
now = utcnow()
if self._ready_for_snapshot(now):
url = self.device.snapshot_url
url = self._device.snapshot_url
try:
response = requests.get(url)

View File

@ -183,7 +183,7 @@ class ONVIFHassCamera(Camera):
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
async def async_added_to_hass(self):
"""Callback when entity is added to hass."""
"""Handle entity addition to hass."""
if ONVIF_DATA not in self.hass.data:
self.hass.data[ONVIF_DATA] = {}
self.hass.data[ONVIF_DATA][ENTITIES] = []

View File

@ -21,6 +21,8 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field'
@ -111,7 +113,7 @@ class PushCamera(Camera):
@property
def state(self):
"""Current state of the camera."""
"""Return current state of the camera."""
return self._state
async def update_image(self, image, filename):

View File

@ -6,10 +6,10 @@
},
"step": {
"confirm": {
"description": "Do you want to setup Google Cast?",
"description": "Do you want to set up Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}
}

View File

@ -4,7 +4,7 @@
"step": {
"confirm": {
"title": "Google Cast",
"description": "Do you want to setup Google Cast?"
"description": "Do you want to set up Google Cast?"
}
},
"abort": {

View File

@ -294,7 +294,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Setup a config entry."""
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
@ -320,7 +320,7 @@ class ClimateDevice(Entity):
@property
def precision(self):
"""Return the precision of the system."""
if self.unit_of_measurement == TEMP_CELSIUS:
if self.hass.config.units.temperature_unit == TEMP_CELSIUS:
return PRECISION_TENTHS
return PRECISION_WHOLE
@ -394,11 +394,6 @@ class ClimateDevice(Entity):
return data
@property
def unit_of_measurement(self):
"""Return the unit of measurement to display."""
return self.hass.config.units.temperature_unit
@property
def temperature_unit(self):
"""Return the unit of measurement used by the platform."""

View File

@ -16,9 +16,8 @@ from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_UNKNOWN)
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID,
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_UNKNOWN)
from homeassistant.helpers import condition
from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval)
@ -297,11 +296,8 @@ class GenericThermostat(ClimateDevice):
@callback
def _async_update_temp(self, state):
"""Update thermostat with latest state from sensor."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
try:
self._cur_temp = self.hass.config.units.temperature(
float(state.state), unit)
self._cur_temp = float(state.state)
except ValueError as ex:
_LOGGER.error("Unable to update from sensor: %s", ex)

View File

@ -58,7 +58,6 @@ class HeatmiserV3Thermostat(ClimateDevice):
def __init__(self, heatmiser, device, name, serport):
"""Initialize the thermostat."""
self.heatmiser = heatmiser
self.device = device
self.serport = serport
self._current_temperature = None
self._name = name

View File

@ -5,12 +5,13 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematic/
"""
import logging
from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
ClimateDevice)
from homeassistant.components.homematic import (
HMDevice, ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT)
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice)
from homeassistant.const import ATTR_TEMPERATURE, STATE_UNKNOWN, TEMP_CELSIUS
DEPENDENCIES = ['homematic']

View File

@ -1,19 +1,18 @@
"""
Support for HomematicIP climate.
Support for HomematicIP Cloud climate devices.
For more details about this component, please refer to the documentation at
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematicip_cloud/
"""
import logging
from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE,
STATE_AUTO, STATE_MANUAL)
from homeassistant.const import TEMP_CELSIUS
ATTR_TEMPERATURE, STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE,
ClimateDevice)
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
HMIPC_HAPID)
HMIPC_HAPID, HomematicipGenericDevice)
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.const import TEMP_CELSIUS
_LOGGER = logging.getLogger(__name__)
@ -27,9 +26,9 @@ HA_STATE_TO_HMIP = {
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."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the HomematicIP Cloud climate devices."""
pass
@ -48,7 +47,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
"""Representation of a MomematicIP heating group."""
"""Representation of a HomematicIP heating group."""
def __init__(self, home, device):
"""Initialize heating group."""

View File

@ -118,17 +118,15 @@ class KNXClimate(ClimateDevice):
def __init__(self, hass, device):
"""Initialize of a KNX climate device."""
self.device = device
self._device = device
self.hass = hass
self.async_register_callbacks()
self._unit_of_measurement = TEMP_CELSIUS
@property
def supported_features(self):
"""Return the list of supported features."""
support = SUPPORT_TARGET_TEMPERATURE
if self.device.supports_operation_mode:
if self._device.supports_operation_mode:
support |= SUPPORT_OPERATION_MODE
return support
@ -137,12 +135,12 @@ class KNXClimate(ClimateDevice):
async def after_update_callback(device):
"""Call after device was updated."""
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
self._device.register_device_updated_cb(after_update_callback)
@property
def name(self):
"""Return the name of the KNX device."""
return self.device.name
return self._device.name
@property
def available(self):
@ -157,46 +155,46 @@ class KNXClimate(ClimateDevice):
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self.device.temperature.value
return self._device.temperature.value
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return self.device.setpoint_shift_step
return self._device.setpoint_shift_step
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self.device.target_temperature.value
return self._device.target_temperature.value
@property
def min_temp(self):
"""Return the minimum temperature."""
return self.device.target_temperature_min
return self._device.target_temperature_min
@property
def max_temp(self):
"""Return the maximum temperature."""
return self.device.target_temperature_max
return self._device.target_temperature_max
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
await self.device.set_target_temperature(temperature)
await self._device.set_target_temperature(temperature)
await self.async_update_ha_state()
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.device.supports_operation_mode:
return self.device.operation_mode.value
if self._device.supports_operation_mode:
return self._device.operation_mode.value
return None
@property
@ -204,11 +202,11 @@ class KNXClimate(ClimateDevice):
"""Return the list of available operation modes."""
return [operation_mode.value for
operation_mode in
self.device.get_supported_operation_modes()]
self._device.get_supported_operation_modes()]
async def async_set_operation_mode(self, operation_mode):
"""Set operation mode."""
if self.device.supports_operation_mode:
if self._device.supports_operation_mode:
from xknx.knx import HVACOperationMode
knx_operation_mode = HVACOperationMode(operation_mode)
await self.device.set_operation_mode(knx_operation_mode)
await self._device.set_operation_mode(knx_operation_mode)

View File

@ -45,7 +45,6 @@ class MaxCubeClimate(ClimateDevice):
def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube ClimateDevice."""
self._name = name
self._unit_of_measurement = TEMP_CELSIUS
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
STATE_VACATION]
self._rf_address = rf_address
@ -81,7 +80,7 @@ class MaxCubeClimate(ClimateDevice):
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
return TEMP_CELSIUS
@property
def current_temperature(self):

View File

@ -166,7 +166,7 @@ class MelissaClimate(ClimateDevice):
self.send({self._api.STATE: self._api.STATE_OFF})
def send(self, value):
"""Sending action to service."""
"""Send action to service."""
try:
old_value = self._cur_settings.copy()
self._cur_settings.update(value)

View File

@ -57,7 +57,7 @@ class NestThermostat(ClimateDevice):
"""Initialize the thermostat."""
self._unit = temp_unit
self.structure = structure
self.device = device
self._device = device
self._fan_list = [STATE_ON, STATE_AUTO]
# Set the default supported features
@ -68,13 +68,13 @@ class NestThermostat(ClimateDevice):
self._operation_list = [STATE_OFF]
# Add supported nest thermostat features
if self.device.can_heat:
if self._device.can_heat:
self._operation_list.append(STATE_HEAT)
if self.device.can_cool:
if self._device.can_cool:
self._operation_list.append(STATE_COOL)
if self.device.can_heat and self.device.can_cool:
if self._device.can_heat and self._device.can_cool:
self._operation_list.append(STATE_AUTO)
self._support_flags = (self._support_flags |
SUPPORT_TARGET_TEMPERATURE_HIGH |
@ -83,7 +83,7 @@ class NestThermostat(ClimateDevice):
self._operation_list.append(STATE_ECO)
# feature of device
self._has_fan = self.device.has_fan
self._has_fan = self._device.has_fan
if self._has_fan:
self._support_flags = (self._support_flags | SUPPORT_FAN_MODE)
@ -124,8 +124,8 @@ class NestThermostat(ClimateDevice):
@property
def unique_id(self):
"""Unique ID for this device."""
return self.device.serial
"""Return unique ID for this device."""
return self._device.serial
@property
def name(self):
@ -202,7 +202,7 @@ class NestThermostat(ClimateDevice):
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
try:
if temp is not None:
self.device.target = temp
self._device.target = temp
except nest.nest.APIError as api_error:
_LOGGER.error("An error occurred while setting temperature: %s",
api_error)
@ -220,7 +220,7 @@ class NestThermostat(ClimateDevice):
_LOGGER.error(
"An error occurred while setting device mode. "
"Invalid operation mode: %s", operation_mode)
self.device.mode = device_mode
self._device.mode = device_mode
@property
def operation_list(self):
@ -254,7 +254,7 @@ class NestThermostat(ClimateDevice):
def set_fan_mode(self, fan_mode):
"""Turn fan on/off."""
if self._has_fan:
self.device.fan = fan_mode.lower()
self._device.fan = fan_mode.lower()
@property
def min_temp(self):
@ -268,20 +268,20 @@ class NestThermostat(ClimateDevice):
def update(self):
"""Cache value from Python-nest."""
self._location = self.device.where
self._name = self.device.name
self._humidity = self.device.humidity
self._temperature = self.device.temperature
self._mode = self.device.mode
self._target_temperature = self.device.target
self._fan = self.device.fan
self._location = self._device.where
self._name = self._device.name
self._humidity = self._device.humidity
self._temperature = self._device.temperature
self._mode = self._device.mode
self._target_temperature = self._device.target
self._fan = self._device.fan
self._away = self.structure.away == 'away'
self._eco_temperature = self.device.eco_temperature
self._locked_temperature = self.device.locked_temperature
self._min_temperature = self.device.min_temperature
self._max_temperature = self.device.max_temperature
self._is_locked = self.device.is_locked
if self.device.temperature_scale == 'C':
self._eco_temperature = self._device.eco_temperature
self._locked_temperature = self._device.locked_temperature
self._min_temperature = self._device.min_temperature
self._max_temperature = self._device.max_temperature
self._is_locked = self._device.is_locked
if self._device.temperature_scale == 'C':
self._temperature_scale = TEMP_CELSIUS
else:
self._temperature_scale = TEMP_FAHRENHEIT

View File

@ -120,7 +120,7 @@ class RadioThermostat(ClimateDevice):
def __init__(self, device, hold_temp, away_temps):
"""Initialize the thermostat."""
self.device = device
self._device = device
self._target_temperature = None
self._current_temperature = None
self._current_operation = STATE_IDLE
@ -137,8 +137,8 @@ class RadioThermostat(ClimateDevice):
# Fan circulate mode is only supported by the CT80 models.
import radiotherm
self._is_model_ct80 = isinstance(self.device,
radiotherm.thermostat.CT80)
self._is_model_ct80 = isinstance(
self._device, radiotherm.thermostat.CT80)
@property
def supported_features(self):
@ -194,7 +194,7 @@ class RadioThermostat(ClimateDevice):
"""Turn fan on/off."""
code = FAN_MODE_TO_CODE.get(fan_mode, None)
if code is not None:
self.device.fmode = code
self._device.fmode = code
@property
def current_temperature(self):
@ -234,15 +234,15 @@ class RadioThermostat(ClimateDevice):
# First time - get the name from the thermostat. This is
# normally set in the radio thermostat web app.
if self._name is None:
self._name = self.device.name['raw']
self._name = self._device.name['raw']
# Request the current state from the thermostat.
data = self.device.tstat['raw']
data = self._device.tstat['raw']
current_temp = data['temp']
if current_temp == -1:
_LOGGER.error('%s (%s) was busy (temp == -1)', self._name,
self.device.host)
self._device.host)
return
# Map thermostat values into various STATE_ flags.
@ -277,30 +277,30 @@ class RadioThermostat(ClimateDevice):
temperature = round_temp(temperature)
if self._current_operation == STATE_COOL:
self.device.t_cool = temperature
self._device.t_cool = temperature
elif self._current_operation == STATE_HEAT:
self.device.t_heat = temperature
self._device.t_heat = temperature
elif self._current_operation == STATE_AUTO:
if self._tstate == STATE_COOL:
self.device.t_cool = temperature
self._device.t_cool = temperature
elif self._tstate == STATE_HEAT:
self.device.t_heat = temperature
self._device.t_heat = temperature
# Only change the hold if requested or if hold mode was turned
# on and we haven't set it yet.
if kwargs.get('hold_changed', False) or not self._hold_set:
if self._hold_temp or self._away:
self.device.hold = 1
self._device.hold = 1
self._hold_set = True
else:
self.device.hold = 0
self._device.hold = 0
def set_time(self):
"""Set device time."""
# Calling this clears any local temperature override and
# reverts to the scheduled temperature.
now = datetime.datetime.now()
self.device.time = {
self._device.time = {
'day': now.weekday(),
'hour': now.hour,
'minute': now.minute
@ -309,13 +309,13 @@ class RadioThermostat(ClimateDevice):
def set_operation_mode(self, operation_mode):
"""Set operation mode (auto, cool, heat, off)."""
if operation_mode in (STATE_OFF, STATE_AUTO):
self.device.tmode = TEMP_MODE_TO_CODE[operation_mode]
self._device.tmode = TEMP_MODE_TO_CODE[operation_mode]
# Setting t_cool or t_heat automatically changes tmode.
elif operation_mode == STATE_COOL:
self.device.t_cool = self._target_temperature
self._device.t_cool = self._target_temperature
elif operation_mode == STATE_HEAT:
self.device.t_heat = self._target_temperature
self._device.t_heat = self._target_temperature
def turn_away_mode_on(self):
"""Turn away on.

View File

@ -71,7 +71,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
for dev in (
yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)):
if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]:
devices.append(SensiboClimate(client, dev))
devices.append(SensiboClimate(
client, dev, hass.config.units.temperature_unit))
except (aiohttp.client_exceptions.ClientConnectorError,
asyncio.TimeoutError):
_LOGGER.exception('Failed to connect to Sensibo servers.')
@ -106,7 +107,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class SensiboClimate(ClimateDevice):
"""Representation of a Sensibo device."""
def __init__(self, client, data):
def __init__(self, client, data, units):
"""Build SensiboClimate.
client: aiohttp session.
@ -115,6 +116,7 @@ class SensiboClimate(ClimateDevice):
self._client = client
self._id = data['id']
self._external_state = None
self._units = units
self._do_update(data)
@property
@ -139,7 +141,7 @@ class SensiboClimate(ClimateDevice):
self._temperatures_list = self._current_capabilities[
'temperatures'].get(temperature_unit_key, {}).get('values', [])
else:
self._temperature_unit = self.unit_of_measurement
self._temperature_unit = self._units
self._temperatures_list = []
self._supported_features = 0
for key in self._ac_states:
@ -175,7 +177,7 @@ class SensiboClimate(ClimateDevice):
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
if self.temperature_unit == self.unit_of_measurement:
if self.temperature_unit == self.hass.config.units.temperature_unit:
# We are working in same units as the a/c unit. Use whole degrees
# like the API supports.
return 1

View File

@ -100,7 +100,7 @@ class ZhongHongClimate(ClimateDevice):
async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADDED)
def _after_update(self, climate):
"""Callback to update state."""
"""Handle state update."""
_LOGGER.debug("async update ha state")
if self._device.current_operation:
self._current_operation = self._device.current_operation.lower()

View File

@ -101,7 +101,7 @@ def websocket_update_entity(hass, connection, msg):
@callback
def _entry_dict(entry):
"""Helper to convert entry to API format."""
"""Convert entry to API format."""
return {
'entity_id': entry.entity_id,
'name': entry.name

View File

@ -212,7 +212,7 @@ class ZWaveProtectionView(HomeAssistantView):
network = hass.data.get(const.DATA_NETWORK)
def _fetch_protection():
"""Helper to get protection data."""
"""Get protection data."""
node = network.nodes.get(nodeid)
if node is None:
return self.json_message('Node not found', HTTP_NOT_FOUND)
@ -236,7 +236,7 @@ class ZWaveProtectionView(HomeAssistantView):
protection_data = await request.json()
def _set_protection():
"""Helper to get protection data."""
"""Set protection data."""
node = network.nodes.get(nodeid)
selection = protection_data["selection"]
value_id = int(protection_data[const.ATTR_VALUE_ID])

View File

@ -18,7 +18,7 @@ from homeassistant.components.cover import (
)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['brunt==0.1.2']
REQUIREMENTS = ['brunt==0.1.3']
_LOGGER = logging.getLogger(__name__)

View File

@ -6,9 +6,9 @@ https://home-assistant.io/components/cover.homematic/
"""
import logging
from homeassistant.components.cover import CoverDevice, ATTR_POSITION,\
ATTR_TILT_POSITION
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.components.cover import (
ATTR_POSITION, ATTR_TILT_POSITION, CoverDevice)
from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)

View File

@ -96,7 +96,7 @@ class KNXCover(CoverDevice):
def __init__(self, hass, device):
"""Initialize the cover."""
self.device = device
self._device = device
self.hass = hass
self.async_register_callbacks()
@ -108,12 +108,12 @@ class KNXCover(CoverDevice):
async def after_update_callback(device):
"""Call after device was updated."""
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
self._device.register_device_updated_cb(after_update_callback)
@property
def name(self):
"""Return the name of the KNX device."""
return self.device.name
return self._device.name
@property
def available(self):
@ -130,56 +130,56 @@ class KNXCover(CoverDevice):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
SUPPORT_SET_POSITION | SUPPORT_STOP
if self.device.supports_angle:
if self._device.supports_angle:
supported_features |= SUPPORT_SET_TILT_POSITION
return supported_features
@property
def current_cover_position(self):
"""Return the current position of the cover."""
return self.device.current_position()
return self._device.current_position()
@property
def is_closed(self):
"""Return if the cover is closed."""
return self.device.is_closed()
return self._device.is_closed()
async def async_close_cover(self, **kwargs):
"""Close the cover."""
if not self.device.is_closed():
await self.device.set_down()
if not self._device.is_closed():
await self._device.set_down()
self.start_auto_updater()
async def async_open_cover(self, **kwargs):
"""Open the cover."""
if not self.device.is_open():
await self.device.set_up()
if not self._device.is_open():
await self._device.set_up()
self.start_auto_updater()
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
await self.device.set_position(position)
await self._device.set_position(position)
self.start_auto_updater()
async def async_stop_cover(self, **kwargs):
"""Stop the cover."""
await self.device.stop()
await self._device.stop()
self.stop_auto_updater()
@property
def current_cover_tilt_position(self):
"""Return current tilt position of cover."""
if not self.device.supports_angle:
if not self._device.supports_angle:
return None
return self.device.current_angle()
return self._device.current_angle()
async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
if ATTR_TILT_POSITION in kwargs:
tilt_position = kwargs[ATTR_TILT_POSITION]
await self.device.set_angle(tilt_position)
await self._device.set_angle(tilt_position)
def start_auto_updater(self):
"""Start the autoupdater to update HASS while cover is moving."""
@ -197,7 +197,7 @@ class KNXCover(CoverDevice):
def auto_updater_hook(self, now):
"""Call for the autoupdater."""
self.async_schedule_update_ha_state()
if self.device.position_reached():
if self._device.position_reached():
self.stop_auto_updater()
self.hass.add_job(self.device.auto_stop_if_necessary())
self.hass.add_job(self._device.auto_stop_if_necessary())

View File

@ -28,19 +28,19 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
@property
def is_closed(self):
"""Return the current position of the cover."""
return self.device.is_down
return self._device.is_down
def close_cover(self, **kwargs):
"""Close the cover."""
self.device.down()
self._device.down()
self.changed()
def open_cover(self, **kwargs):
"""Open the cover."""
self.device.up()
self._device.up()
self.changed()
def stop_cover(self, **kwargs):
"""Stop the cover."""
self.device.stop()
self._device.stop()
self.changed()

View File

@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry):
@callback
def async_add_device_callback(device_type, device):
"""Called when a new device has been created in deCONZ."""
"""Handle event of new device creation in deCONZ."""
async_dispatcher_send(
hass, 'deconz_new_{}'.format(device_type), [device])
@ -105,7 +105,7 @@ async def async_setup_entry(hass, config_entry):
@callback
def async_add_remote(sensors):
"""Setup remote from deCONZ."""
"""Set up remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors:

View File

@ -330,19 +330,18 @@ class DeviceTracker:
})
# update known_devices.yaml
self.hass.async_add_job(
self.hass.async_create_task(
self.async_update_config(
self.hass.config.path(YAML_DEVICES), dev_id, device)
)
@asyncio.coroutine
def async_update_config(self, path, dev_id, device):
async def async_update_config(self, path, dev_id, device):
"""Add device to YAML configuration file.
This method is a coroutine.
"""
with (yield from self._is_updating):
yield from self.hass.async_add_job(
async with self._is_updating:
await self.hass.async_add_executor_job(
update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device)
@ -681,8 +680,7 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
# Initial scan of each mac we also tell about host name for config
seen = set() # type: Any
@asyncio.coroutine
def async_device_tracker_scan(now: dt_util.dt.datetime):
async def async_device_tracker_scan(now: dt_util.dt.datetime):
"""Handle interval matches."""
if update_lock.locked():
_LOGGER.warning(
@ -690,18 +688,18 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
"scan interval %s", platform, interval)
return
with (yield from update_lock):
found_devices = yield from scanner.async_scan_devices()
async with update_lock:
found_devices = await scanner.async_scan_devices()
for mac in found_devices:
if mac in seen:
host_name = None
else:
host_name = yield from scanner.async_get_device_name(mac)
host_name = await scanner.async_get_device_name(mac)
seen.add(mac)
try:
extra_attributes = (yield from
extra_attributes = (await
scanner.async_get_extra_attributes(mac))
except NotImplementedError:
extra_attributes = dict()

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME
)
REQUIREMENTS = ['ndms2_client==0.0.3']
REQUIREMENTS = ['ndms2_client==0.0.4']
_LOGGER = logging.getLogger(__name__)

View File

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.event import track_utc_time_change
REQUIREMENTS = ['ritassist==0.5']
REQUIREMENTS = ['ritassist==0.9.2']
_LOGGER = logging.getLogger(__name__)
@ -57,7 +57,7 @@ class RitAssistDeviceScanner:
config.get(CONF_PASSWORD))
def setup(self, hass):
"""Setup a timer and start gathering devices."""
"""Set up a timer and start gathering devices."""
self._refresh()
track_utc_time_change(hass,
lambda now: self._refresh(),
@ -78,6 +78,10 @@ class RitAssistDeviceScanner:
for device in devices:
if (not self._include or
device.license_plate in self._include):
if device.active or device.current_address is None:
device.get_map_details()
self._see(dev_id=device.plate_as_id,
gps=(device.latitude, device.longitude),
attributes=device.state_attributes,

View File

@ -69,11 +69,16 @@ class TplinkDeviceScanner(DeviceScanner):
password = config[CONF_PASSWORD]
username = config[CONF_USERNAME]
self.tplink_client = TpLinkClient(
password, host=host, username=username)
self.success_init = False
try:
self.tplink_client = TpLinkClient(
password, host=host, username=username)
self.last_results = {}
self.success_init = self._update_info()
self.last_results = {}
self.success_init = self._update_info()
except requests.exceptions.ConnectionError:
_LOGGER.debug("ConnectionError in TplinkDeviceScanner")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
@ -115,7 +120,11 @@ class Tplink1DeviceScanner(DeviceScanner):
self.password = password
self.last_results = {}
self.success_init = self._update_info()
self.success_init = False
try:
self.success_init = self._update_info()
except requests.exceptions.ConnectionError:
_LOGGER.debug("ConnectionError in Tplink1DeviceScanner")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""

View File

@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
})
REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
def get_scanner(hass, config):
@ -73,5 +73,8 @@ class XiaomiMiioDeviceScanner(DeviceScanner):
return devices
async def async_get_device_name(self, device):
"""The repeater doesn't provide the name of the associated device."""
"""Return None.
The repeater doesn't provide the name of the associated device.
"""
return None

View File

@ -86,11 +86,11 @@ SERVICE_HANDLERS = {
'volumio': ('media_player', 'volumio'),
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
'freebox': ('device_tracker', 'freebox'),
'dlna_dmr': ('media_player', 'dlna_dmr'),
}
OPTIONAL_SERVICE_HANDLERS = {
SERVICE_HOMEKIT: ('homekit_controller', None),
'dlna_dmr': ('media_player', 'dlna_dmr'),
}
CONF_IGNORE = 'ignore'

View File

@ -139,17 +139,17 @@ class ConfiguredDoorbird():
@property
def name(self):
"""Custom device name."""
"""Get custom device name."""
return self._name
@property
def device(self):
"""The configured device."""
"""Get the configured device."""
return self._device
@property
def custom_url(self):
"""Custom url for device."""
"""Get custom url for device."""
return self._custom_url
@property

View File

@ -0,0 +1,87 @@
"""Parent component for Ecovacs Deebot vacuums.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/ecovacs/
"""
import logging
import random
import string
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \
EVENT_HOMEASSISTANT_STOP
REQUIREMENTS = ['sucks==0.9.1']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "ecovacs"
CONF_COUNTRY = "country"
CONF_CONTINENT = "continent"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string),
vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string),
})
}, extra=vol.ALLOW_EXTRA)
ECOVACS_DEVICES = "ecovacs_devices"
# Generate a random device ID on each bootup
ECOVACS_API_DEVICEID = ''.join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(8)
)
def setup(hass, config):
"""Set up the Ecovacs component."""
_LOGGER.debug("Creating new Ecovacs component")
hass.data[ECOVACS_DEVICES] = []
from sucks import EcoVacsAPI, VacBot
ecovacs_api = EcoVacsAPI(ECOVACS_API_DEVICEID,
config[DOMAIN].get(CONF_USERNAME),
EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)),
config[DOMAIN].get(CONF_COUNTRY),
config[DOMAIN].get(CONF_CONTINENT))
devices = ecovacs_api.devices()
_LOGGER.debug("Ecobot devices: %s", devices)
for device in devices:
_LOGGER.info("Discovered Ecovacs device on account: %s",
device['nick'])
vacbot = VacBot(ecovacs_api.uid,
ecovacs_api.REALM,
ecovacs_api.resource,
ecovacs_api.user_access_token,
device,
config[DOMAIN].get(CONF_CONTINENT).lower(),
monitor=True)
hass.data[ECOVACS_DEVICES].append(vacbot)
def stop(event: object) -> None:
"""Shut down open connections to Ecovacs XMPP server."""
for device in hass.data[ECOVACS_DEVICES]:
_LOGGER.info("Shutting down connection to Ecovacs device %s",
device.vacuum['nick'])
device.disconnect()
# Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
if hass.data[ECOVACS_DEVICES]:
_LOGGER.debug("Starting vacuum components")
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)
return True

View File

@ -101,7 +101,7 @@ def setup(hass, config):
server.start()
def handle_stop_event(event):
"""Callback function for HA stop event."""
"""Handle HA stop event."""
server.stop()
# listen to home assistant stop event

View File

@ -2,7 +2,7 @@
Support for INSTEON fans via PowerLinc Modem.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan.insteon_plm/
https://home-assistant.io/components/fan.insteon/
"""
import asyncio
import logging
@ -14,9 +14,9 @@ from homeassistant.components.fan import (SPEED_OFF,
FanEntity,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_OFF
from homeassistant.components.insteon_plm import InsteonPLMEntity
from homeassistant.components.insteon import InsteonEntity
DEPENDENCIES = ['insteon_plm']
DEPENDENCIES = ['insteon']
SPEED_TO_HEX = {SPEED_OFF: 0x00,
SPEED_LOW: 0x3f,
@ -30,22 +30,22 @@ _LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm'].get('plm')
"""Set up the INSTEON device class for the hass platform."""
insteon_modem = hass.data['insteon'].get('modem')
address = discovery_info['address']
device = plm.devices[address]
device = insteon_modem.devices[address]
state_key = discovery_info['state_key']
_LOGGER.debug('Adding device %s entity %s to Fan platform',
device.address.hex, device.states[state_key].name)
new_entity = InsteonPLMFan(device, state_key)
new_entity = InsteonFan(device, state_key)
async_add_devices([new_entity])
class InsteonPLMFan(InsteonPLMEntity, FanEntity):
class InsteonFan(InsteonEntity, FanEntity):
"""An INSTEON fan component."""
@property

View File

@ -1,107 +0,0 @@
"""
Support for Insteon fans via local hub control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan.insteon_local/
"""
import logging
from datetime import timedelta
from homeassistant import util
from homeassistant.components.fan import (
ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED, FanEntity)
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['insteon_local']
DOMAIN = 'fan'
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
SUPPORT_INSTEON_LOCAL = SUPPORT_SET_SPEED
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon local fan platform."""
insteonhub = hass.data['insteon_local']
if discovery_info is None:
return
linked = discovery_info['linked']
device_list = []
for device_id in linked:
if (linked[device_id]['cat_type'] == 'dimmer' and
linked[device_id]['sku'] == '2475F'):
device = insteonhub.fan(device_id)
device_list.append(
InsteonLocalFanDevice(device)
)
add_devices(device_list)
class InsteonLocalFanDevice(FanEntity):
"""An abstract Class for an Insteon node."""
def __init__(self, node):
"""Initialize the device."""
self.node = node
self._speed = SPEED_OFF
@property
def name(self):
"""Return the name of the node."""
return self.node.device_id
@property
def unique_id(self):
"""Return the ID of this Insteon node."""
return self.node.device_id
@property
def speed(self) -> str:
"""Return the current speed."""
return self._speed
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
"""Update state of the fan."""
resp = self.node.status()
if 'cmd2' in resp:
if resp['cmd2'] == '00':
self._speed = SPEED_OFF
elif resp['cmd2'] == '55':
self._speed = SPEED_LOW
elif resp['cmd2'] == 'AA':
self._speed = SPEED_MEDIUM
elif resp['cmd2'] == 'FF':
self._speed = SPEED_HIGH
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_INSTEON_LOCAL
def turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn device on."""
if speed is None:
speed = kwargs.get(ATTR_SPEED, SPEED_MEDIUM)
self.set_speed(speed)
def turn_off(self, **kwargs) -> None:
"""Turn device off."""
self.node.off()
def set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self.node.on(speed):
self._speed = speed

View File

@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'zhimi.humidifier.ca1']),
})
REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
REQUIREMENTS = ['python-miio==0.4.1', 'construct==2.9.41']
ATTR_MODEL = 'model'

View File

@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180816.0']
REQUIREMENTS = ['home-assistant-frontend==20180820.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',

View File

@ -77,7 +77,7 @@ class _GoogleEntity:
domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
return [Trait(state) for Trait in trait.TRAITS
return [Trait(self.hass, state) for Trait in trait.TRAITS
if Trait.supported(domain, features)]
@callback
@ -159,7 +159,7 @@ class _GoogleEntity:
executed = False
for trt in self.traits():
if trt.can_execute(command, params):
await trt.execute(self.hass, command, params)
await trt.execute(command, params)
executed = True
break

View File

@ -14,7 +14,6 @@ from homeassistant.components import (
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
@ -50,15 +49,14 @@ TRAITS = []
def register_trait(trait):
"""Decorator to register a trait."""
"""Decorate a function to register a trait."""
TRAITS.append(trait)
return trait
def _google_temp_unit(state):
def _google_temp_unit(units):
"""Return Google temperature unit."""
if (state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) ==
TEMP_FAHRENHEIT):
if units == TEMP_FAHRENHEIT:
return 'F'
return 'C'
@ -68,8 +66,9 @@ class _Trait:
commands = []
def __init__(self, state):
def __init__(self, hass, state):
"""Initialize a trait for a state."""
self.hass = hass
self.state = state
def sync_attributes(self):
@ -84,7 +83,7 @@ class _Trait:
"""Test if command can be executed."""
return command in self.commands
async def execute(self, hass, command, params):
async def execute(self, command, params):
"""Execute a trait command."""
raise NotImplementedError
@ -141,24 +140,24 @@ class BrightnessTrait(_Trait):
return response
async def execute(self, hass, command, params):
async def execute(self, command, params):
"""Execute a brightness command."""
domain = self.state.domain
if domain == light.DOMAIN:
await hass.services.async_call(
await self.hass.services.async_call(
light.DOMAIN, light.SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_BRIGHTNESS_PCT: params['brightness']
}, blocking=True)
elif domain == cover.DOMAIN:
await hass.services.async_call(
await self.hass.services.async_call(
cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, {
ATTR_ENTITY_ID: self.state.entity_id,
cover.ATTR_POSITION: params['brightness']
}, blocking=True)
elif domain == media_player.DOMAIN:
await hass.services.async_call(
await self.hass.services.async_call(
media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, {
ATTR_ENTITY_ID: self.state.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL:
@ -201,7 +200,7 @@ class OnOffTrait(_Trait):
return {'on': self.state.state != cover.STATE_CLOSED}
return {'on': self.state.state != STATE_OFF}
async def execute(self, hass, command, params):
async def execute(self, command, params):
"""Execute an OnOff command."""
domain = self.state.domain
@ -220,7 +219,7 @@ class OnOffTrait(_Trait):
service_domain = domain
service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF
await hass.services.async_call(service_domain, service, {
await self.hass.services.async_call(service_domain, service, {
ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True)
@ -268,14 +267,14 @@ class ColorSpectrumTrait(_Trait):
return (command in self.commands and
'spectrumRGB' in params.get('color', {}))
async def execute(self, hass, command, params):
async def execute(self, command, params):
"""Execute a color spectrum command."""
# Convert integer to hex format and left pad with 0's till length 6
hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
color = color_util.color_RGB_to_hs(
*color_util.rgb_hex_to_rgb_list(hex_value))
await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
await self.hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_HS_COLOR: color
}, blocking=True)
@ -331,7 +330,7 @@ class ColorTemperatureTrait(_Trait):
return (command in self.commands and
'temperature' in params.get('color', {}))
async def execute(self, hass, command, params):
async def execute(self, command, params):
"""Execute a color temperature command."""
temp = color_util.color_temperature_kelvin_to_mired(
params['color']['temperature'])
@ -344,7 +343,7 @@ class ColorTemperatureTrait(_Trait):
"Temperature should be between {} and {}".format(min_temp,
max_temp))
await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
await self.hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_COLOR_TEMP: temp,
}, blocking=True)
@ -376,12 +375,13 @@ class SceneTrait(_Trait):
"""Return scene query attributes."""
return {}
async def execute(self, hass, command, params):
async def execute(self, command, params):
"""Execute a scene command."""
# Don't block for scripts as they can be slow.
await hass.services.async_call(self.state.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id
}, blocking=self.state.domain != script.DOMAIN)
await self.hass.services.async_call(
self.state.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id
}, blocking=self.state.domain != script.DOMAIN)
@register_trait
@ -425,7 +425,8 @@ class TemperatureSettingTrait(_Trait):
return {
'availableThermostatModes': ','.join(modes),
'thermostatTemperatureUnit': _google_temp_unit(self.state),
'thermostatTemperatureUnit': _google_temp_unit(
self.hass.config.units.temperature_unit)
}
def query_attributes(self):
@ -437,7 +438,7 @@ class TemperatureSettingTrait(_Trait):
if operation is not None and operation in self.hass_to_google:
response['thermostatMode'] = self.hass_to_google[operation]
unit = self.state.attributes[ATTR_UNIT_OF_MEASUREMENT]
unit = self.hass.config.units.temperature_unit
current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE)
if current_temp is not None:
@ -465,10 +466,10 @@ class TemperatureSettingTrait(_Trait):
return response
async def execute(self, hass, command, params):
async def execute(self, command, params):
"""Execute a temperature point or mode command."""
# All sent in temperatures are always in Celsius
unit = self.state.attributes[ATTR_UNIT_OF_MEASUREMENT]
unit = self.hass.config.units.temperature_unit
min_temp = self.state.attributes[climate.ATTR_MIN_TEMP]
max_temp = self.state.attributes[climate.ATTR_MAX_TEMP]
@ -482,7 +483,7 @@ class TemperatureSettingTrait(_Trait):
"Temperature should be between {} and {}".format(min_temp,
max_temp))
await hass.services.async_call(
await self.hass.services.async_call(
climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {
ATTR_ENTITY_ID: self.state.entity_id,
climate.ATTR_TEMPERATURE: temp
@ -508,7 +509,7 @@ class TemperatureSettingTrait(_Trait):
"Lower bound for temperature range should be between "
"{} and {}".format(min_temp, max_temp))
await hass.services.async_call(
await self.hass.services.async_call(
climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {
ATTR_ENTITY_ID: self.state.entity_id,
climate.ATTR_TARGET_TEMP_HIGH: temp_high,
@ -516,7 +517,7 @@ class TemperatureSettingTrait(_Trait):
}, blocking=True)
elif command == COMMAND_THERMOSTAT_SET_MODE:
await hass.services.async_call(
await self.hass.services.async_call(
climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE, {
ATTR_ENTITY_ID: self.state.entity_id,
climate.ATTR_OPERATION_MODE:

View File

@ -550,12 +550,12 @@ class Group(Entity):
self._async_update_group_state()
async def async_added_to_hass(self):
"""Callback when added to HASS."""
"""Handle addition to HASS."""
if self.tracking:
self.async_start()
async def async_will_remove_from_hass(self):
"""Callback when removed from HASS."""
"""Handle removal from HASS."""
if self._async_unsub_state_changed:
self._async_unsub_state_changed()
self._async_unsub_state_changed = None

View File

@ -0,0 +1,31 @@
{
"config": {
"abort": {
"already_configured": "Google Hangouts is already configured",
"unknown": "Unknown error occurred."
},
"error": {
"invalid_2fa": "Invalid 2 Factor Authorization, please try again.",
"invalid_2fa_method": "Invalig 2FA Method (Verify on Phone).",
"invalid_login": "Invalid Login, please try again."
},
"step": {
"2fa": {
"data": {
"2fa": "2FA Pin"
},
"description": "",
"title": "2-Factor-Authorization"
},
"user": {
"data": {
"email": "E-Mail Address",
"password": "Password"
},
"description": "",
"title": "Google Hangouts Login"
}
},
"title": "Google Hangouts"
}
}

View File

@ -0,0 +1,87 @@
"""
The hangouts bot component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/hangouts/
"""
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import dispatcher
from .config_flow import configured_hangouts
from .const import (
CONF_BOT, CONF_COMMANDS, CONF_REFRESH_TOKEN, DOMAIN,
EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE,
SERVICE_UPDATE)
REQUIREMENTS = ['hangups==0.4.5']
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up the Hangouts bot component."""
config = config.get(DOMAIN, [])
hass.data[DOMAIN] = {CONF_COMMANDS: config[CONF_COMMANDS]}
if configured_hangouts(hass) is None:
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}
))
return True
async def async_setup_entry(hass, config):
"""Set up a config entry."""
from hangups.auth import GoogleAuthError
try:
from .hangouts_bot import HangoutsBot
bot = HangoutsBot(
hass,
config.data.get(CONF_REFRESH_TOKEN),
hass.data[DOMAIN][CONF_COMMANDS])
hass.data[DOMAIN][CONF_BOT] = bot
except GoogleAuthError as exception:
_LOGGER.error("Hangouts failed to log in: %s", str(exception))
return False
dispatcher.async_dispatcher_connect(
hass,
EVENT_HANGOUTS_CONNECTED,
bot.async_handle_update_users_and_conversations)
dispatcher.async_dispatcher_connect(
hass,
EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
bot.async_update_conversation_commands)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
bot.async_handle_hass_stop)
await bot.async_connect()
hass.services.async_register(DOMAIN, SERVICE_SEND_MESSAGE,
bot.async_handle_send_message,
schema=MESSAGE_SCHEMA)
hass.services.async_register(DOMAIN,
SERVICE_UPDATE,
bot.
async_handle_update_users_and_conversations,
schema=vol.Schema({}))
return True
async def async_unload_entry(hass, _):
"""Unload a config entry."""
bot = hass.data[DOMAIN].pop(CONF_BOT)
await bot.async_disconnect()
return True

View File

@ -0,0 +1,107 @@
"""Config flow to configure Google Hangouts."""
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import callback
from .const import CONF_2FA, CONF_REFRESH_TOKEN
from .const import DOMAIN as HANGOUTS_DOMAIN
@callback
def configured_hangouts(hass):
"""Return the configures Google Hangouts Account."""
entries = hass.config_entries.async_entries(HANGOUTS_DOMAIN)
if entries:
return entries[0]
return None
@config_entries.HANDLERS.register(HANGOUTS_DOMAIN)
class HangoutsFlowHandler(data_entry_flow.FlowHandler):
"""Config flow Google Hangouts."""
VERSION = 1
def __init__(self):
"""Initialize Google Hangouts config flow."""
self._credentials = None
self._refresh_token = None
async def async_step_user(self, user_input=None):
"""Handle a flow start."""
errors = {}
if configured_hangouts(self.hass) is not None:
return self.async_abort(reason="already_configured")
if user_input is not None:
from hangups import get_auth
from .hangups_utils import (HangoutsCredentials,
HangoutsRefreshToken,
GoogleAuthError, Google2FAError)
self._credentials = HangoutsCredentials(user_input[CONF_EMAIL],
user_input[CONF_PASSWORD])
self._refresh_token = HangoutsRefreshToken(None)
try:
await self.hass.async_add_executor_job(get_auth,
self._credentials,
self._refresh_token)
return await self.async_step_final()
except GoogleAuthError as err:
if isinstance(err, Google2FAError):
return await self.async_step_2fa()
msg = str(err)
if msg == 'Unknown verification code input':
errors['base'] = 'invalid_2fa_method'
else:
errors['base'] = 'invalid_login'
return self.async_show_form(
step_id='user',
data_schema=vol.Schema({
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str
}),
errors=errors
)
async def async_step_2fa(self, user_input=None):
"""Handle the 2fa step, if needed."""
errors = {}
if user_input is not None:
from hangups import get_auth
from .hangups_utils import GoogleAuthError
self._credentials.set_verification_code(user_input[CONF_2FA])
try:
await self.hass.async_add_executor_job(get_auth,
self._credentials,
self._refresh_token)
return await self.async_step_final()
except GoogleAuthError:
errors['base'] = 'invalid_2fa'
return self.async_show_form(
step_id=CONF_2FA,
data_schema=vol.Schema({
vol.Required(CONF_2FA): str,
}),
errors=errors
)
async def async_step_final(self):
"""Handle the final step, create the config entry."""
return self.async_create_entry(
title=self._credentials.get_email(),
data={
CONF_EMAIL: self._credentials.get_email(),
CONF_REFRESH_TOKEN: self._refresh_token.get()
})
async def async_step_import(self, _):
"""Handle a flow import."""
return self.async_abort(reason='already_configured')

View File

@ -0,0 +1,78 @@
"""Constants for Google Hangouts Component."""
import logging
import voluptuous as vol
from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger('homeassistant.components.hangouts')
DOMAIN = 'hangouts'
CONF_2FA = '2fa'
CONF_REFRESH_TOKEN = 'refresh_token'
CONF_BOT = 'bot'
CONF_CONVERSATIONS = 'conversations'
CONF_DEFAULT_CONVERSATIONS = 'default_conversations'
CONF_COMMANDS = 'commands'
CONF_WORD = 'word'
CONF_EXPRESSION = 'expression'
EVENT_HANGOUTS_COMMAND = 'hangouts_command'
EVENT_HANGOUTS_CONNECTED = 'hangouts_connected'
EVENT_HANGOUTS_DISCONNECTED = 'hangouts_disconnected'
EVENT_HANGOUTS_USERS_CHANGED = 'hangouts_users_changed'
EVENT_HANGOUTS_CONVERSATIONS_CHANGED = 'hangouts_conversations_changed'
CONF_CONVERSATION_ID = 'id'
CONF_CONVERSATION_NAME = 'name'
SERVICE_SEND_MESSAGE = 'send_message'
SERVICE_UPDATE = 'update'
TARGETS_SCHEMA = vol.All(
vol.Schema({
vol.Exclusive(CONF_CONVERSATION_ID, 'id or name'): cv.string,
vol.Exclusive(CONF_CONVERSATION_NAME, 'id or name'): cv.string
}),
cv.has_at_least_one_key(CONF_CONVERSATION_ID, CONF_CONVERSATION_NAME)
)
MESSAGE_SEGMENT_SCHEMA = vol.Schema({
vol.Required('text'): cv.string,
vol.Optional('is_bold'): cv.boolean,
vol.Optional('is_italic'): cv.boolean,
vol.Optional('is_strikethrough'): cv.boolean,
vol.Optional('is_underline'): cv.boolean,
vol.Optional('parse_str'): cv.boolean,
vol.Optional('link_target'): cv.string
})
MESSAGE_SCHEMA = vol.Schema({
vol.Required(ATTR_TARGET): [TARGETS_SCHEMA],
vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA]
})
COMMAND_SCHEMA = vol.All(
# Basic Schema
vol.Schema({
vol.Exclusive(CONF_WORD, 'trigger'): cv.string,
vol.Exclusive(CONF_EXPRESSION, 'trigger'): cv.is_regex,
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_CONVERSATIONS): [TARGETS_SCHEMA]
}),
# Make sure it's either a word or an expression command
cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION)
)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_COMMANDS, default=[]): [COMMAND_SCHEMA]
})
}, extra=vol.ALLOW_EXTRA)

View File

@ -0,0 +1,229 @@
"""The Hangouts Bot."""
import logging
import re
from homeassistant.helpers import dispatcher
from .const import (
ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, CONF_EXPRESSION, CONF_NAME,
CONF_WORD, DOMAIN, EVENT_HANGOUTS_COMMAND, EVENT_HANGOUTS_CONNECTED,
EVENT_HANGOUTS_CONVERSATIONS_CHANGED, EVENT_HANGOUTS_DISCONNECTED)
_LOGGER = logging.getLogger(__name__)
class HangoutsBot:
"""The Hangouts Bot."""
def __init__(self, hass, refresh_token, commands):
"""Set up the client."""
self.hass = hass
self._connected = False
self._refresh_token = refresh_token
self._commands = commands
self._word_commands = None
self._expression_commands = None
self._client = None
self._user_list = None
self._conversation_list = None
def _resolve_conversation_name(self, name):
for conv in self._conversation_list.get_all():
if conv.name == name:
return conv
return None
def async_update_conversation_commands(self, _):
"""Refresh the commands for every conversation."""
self._word_commands = {}
self._expression_commands = {}
for command in self._commands:
if command.get(CONF_CONVERSATIONS):
conversations = []
for conversation in command.get(CONF_CONVERSATIONS):
if 'id' in conversation:
conversations.append(conversation['id'])
elif 'name' in conversation:
conversations.append(self._resolve_conversation_name(
conversation['name']).id_)
command['_' + CONF_CONVERSATIONS] = conversations
else:
command['_' + CONF_CONVERSATIONS] = \
[conv.id_ for conv in self._conversation_list.get_all()]
if command.get(CONF_WORD):
for conv_id in command['_' + CONF_CONVERSATIONS]:
if conv_id not in self._word_commands:
self._word_commands[conv_id] = {}
word = command[CONF_WORD].lower()
self._word_commands[conv_id][word] = command
elif command.get(CONF_EXPRESSION):
command['_' + CONF_EXPRESSION] = re.compile(
command.get(CONF_EXPRESSION))
for conv_id in command['_' + CONF_CONVERSATIONS]:
if conv_id not in self._expression_commands:
self._expression_commands[conv_id] = []
self._expression_commands[conv_id].append(command)
try:
self._conversation_list.on_event.remove_observer(
self._handle_conversation_event)
except ValueError:
pass
self._conversation_list.on_event.add_observer(
self._handle_conversation_event)
def _handle_conversation_event(self, event):
from hangups import ChatMessageEvent
if event.__class__ is ChatMessageEvent:
self._handle_conversation_message(
event.conversation_id, event.user_id, event)
def _handle_conversation_message(self, conv_id, user_id, event):
"""Handle a message sent to a conversation."""
user = self._user_list.get_user(user_id)
if user.is_self:
return
_LOGGER.debug("Handling message '%s' from %s",
event.text, user.full_name)
event_data = None
pieces = event.text.split(' ')
cmd = pieces[0].lower()
command = self._word_commands.get(conv_id, {}).get(cmd)
if command:
event_data = {
'command': command[CONF_NAME],
'conversation_id': conv_id,
'user_id': user_id,
'user_name': user.full_name,
'data': pieces[1:]
}
else:
# After single-word commands, check all regex commands in the room
for command in self._expression_commands.get(conv_id, []):
match = command['_' + CONF_EXPRESSION].match(event.text)
if not match:
continue
event_data = {
'command': command[CONF_NAME],
'conversation_id': conv_id,
'user_id': user_id,
'user_name': user.full_name,
'data': match.groupdict()
}
if event_data is not None:
self.hass.bus.fire(EVENT_HANGOUTS_COMMAND, event_data)
async def async_connect(self):
"""Login to the Google Hangouts."""
from .hangups_utils import HangoutsRefreshToken, HangoutsCredentials
from hangups import Client
from hangups import get_auth
session = await self.hass.async_add_executor_job(
get_auth, HangoutsCredentials(None, None, None),
HangoutsRefreshToken(self._refresh_token))
self._client = Client(session)
self._client.on_connect.add_observer(self._on_connect)
self._client.on_disconnect.add_observer(self._on_disconnect)
self.hass.loop.create_task(self._client.connect())
def _on_connect(self):
_LOGGER.debug('Connected!')
self._connected = True
dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_CONNECTED)
def _on_disconnect(self):
"""Handle disconnecting."""
_LOGGER.debug('Connection lost!')
self._connected = False
dispatcher.async_dispatcher_send(self.hass,
EVENT_HANGOUTS_DISCONNECTED)
async def async_disconnect(self):
"""Disconnect the client if it is connected."""
if self._connected:
await self._client.disconnect()
async def async_handle_hass_stop(self, _):
"""Run once when Home Assistant stops."""
await self.async_disconnect()
async def _async_send_message(self, message, targets):
conversations = []
for target in targets:
conversation = None
if 'id' in target:
conversation = self._conversation_list.get(target['id'])
elif 'name' in target:
conversation = self._resolve_conversation_name(target['name'])
if conversation is not None:
conversations.append(conversation)
if not conversations:
return False
from hangups import ChatMessageSegment, hangouts_pb2
messages = []
for segment in message:
if 'parse_str' in segment and segment['parse_str']:
messages.extend(ChatMessageSegment.from_str(segment['text']))
else:
if 'parse_str' in segment:
del segment['parse_str']
messages.append(ChatMessageSegment(**segment))
messages.append(ChatMessageSegment('',
segment_type=hangouts_pb2.
SEGMENT_TYPE_LINE_BREAK))
if not messages:
return False
for conv in conversations:
await conv.send_message(messages)
async def _async_list_conversations(self):
import hangups
self._user_list, self._conversation_list = \
(await hangups.build_user_conversation_list(self._client))
users = {}
conversations = {}
for user in self._user_list.get_all():
users[str(user.id_.chat_id)] = {'full_name': user.full_name,
'is_self': user.is_self}
for conv in self._conversation_list.get_all():
users_in_conversation = {}
for user in conv.users:
users_in_conversation[str(user.id_.chat_id)] = \
{'full_name': user.full_name, 'is_self': user.is_self}
conversations[str(conv.id_)] = \
{'name': conv.name, 'users': users_in_conversation}
self.hass.states.async_set("{}.users".format(DOMAIN),
len(self._user_list.get_all()),
attributes=users)
self.hass.states.async_set("{}.conversations".format(DOMAIN),
len(self._conversation_list.get_all()),
attributes=conversations)
dispatcher.async_dispatcher_send(self.hass,
EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
conversations)
async def async_handle_send_message(self, service):
"""Handle the send_message service."""
await self._async_send_message(service.data[ATTR_MESSAGE],
service.data[ATTR_TARGET])
async def async_handle_update_users_and_conversations(self, _=None):
"""Handle the update_users_and_conversations service."""
await self._async_list_conversations()

View File

@ -0,0 +1,81 @@
"""Utils needed for Google Hangouts."""
from hangups import CredentialsPrompt, GoogleAuthError, RefreshTokenCache
class Google2FAError(GoogleAuthError):
"""A Google authentication request failed."""
class HangoutsCredentials(CredentialsPrompt):
"""Google account credentials.
This implementation gets the user data as params.
"""
def __init__(self, email, password, pin=None):
"""Google account credentials.
:param email: Google account email address.
:param password: Google account password.
:param pin: Google account verification code.
"""
self._email = email
self._password = password
self._pin = pin
def get_email(self):
"""Return email.
:return: Google account email address.
"""
return self._email
def get_password(self):
"""Return password.
:return: Google account password.
"""
return self._password
def get_verification_code(self):
"""Return the verification code.
:return: Google account verification code.
"""
if self._pin is None:
raise Google2FAError()
return self._pin
def set_verification_code(self, pin):
"""Set the verification code.
:param pin: Google account verification code.
"""
self._pin = pin
class HangoutsRefreshToken(RefreshTokenCache):
"""Memory-based cache for refresh token."""
def __init__(self, token):
"""Memory-based cache for refresh token.
:param token: Initial refresh token.
"""
super().__init__("")
self._token = token
def get(self):
"""Get cached refresh token.
:return: Cached refresh token.
"""
return self._token
def set(self, refresh_token):
"""Cache a refresh token.
:param refresh_token: Refresh token to cache.
"""
self._token = refresh_token

View File

@ -0,0 +1,12 @@
update:
description: Updates the list of users and conversations.
send_message:
description: Send a notification to a specific target.
fields:
target:
description: List of targets with id or name. [Required]
example: '[{"id": "UgxrXzVrARmjx_C6AZx4AaABAagBo-6UCw"}, {"name": "Test Conversation"}]'
message:
description: List of message segments, only the "text" field is required in every segment. [Required]
example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]'

View File

@ -0,0 +1,31 @@
{
"config": {
"abort": {
"already_configured": "Google Hangouts is already configured",
"unknown": "Unknown error occurred."
},
"error": {
"invalid_login": "Invalid Login, please try again.",
"invalid_2fa": "Invalid 2 Factor Authorization, please try again.",
"invalid_2fa_method": "Invalig 2FA Method (Verify on Phone)."
},
"step": {
"user": {
"data": {
"email": "E-Mail Address",
"password": "Password"
},
"description": "",
"title": "Google Hangouts Login"
},
"2fa": {
"data": {
"2fa": "2FA Pin"
},
"description": "",
"title": "2-Factor-Authorization"
}
},
"title": "Google Hangouts"
}
}

View File

@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.Schema({
async def async_setup(hass, config):
"""Setup the HomeKit component."""
"""Set up the HomeKit component."""
_LOGGER.debug('Begin setup HomeKit')
conf = config[DOMAIN]
@ -196,7 +196,7 @@ class HomeKit():
self.driver = None
def setup(self):
"""Setup bridge and accessory driver."""
"""Set up bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver
self.hass.bus.async_listen_once(

View File

@ -27,17 +27,17 @@ _LOGGER = logging.getLogger(__name__)
def debounce(func):
"""Decorator function. Debounce callbacks form HomeKit."""
"""Decorate function to debounce callbacks from HomeKit."""
@ha_callback
def call_later_listener(self, *args):
"""Callback listener called from call_later."""
"""Handle call_later callback."""
debounce_params = self.debounce.pop(func.__name__, None)
if debounce_params:
self.hass.async_add_job(func, self, *debounce_params[1:])
@wraps(func)
def wrapper(self, *args):
"""Wrapper starts async timer."""
"""Start async timer."""
debounce_params = self.debounce.pop(func.__name__, None)
if debounce_params:
debounce_params[0]() # remove listener
@ -88,7 +88,7 @@ class HomeAccessory(Accessory):
CHAR_STATUS_LOW_BATTERY, value=0)
async def run(self):
"""Method called by accessory after driver is started.
"""Handle accessory driver started event.
Run inside the HAP-python event loop.
"""
@ -100,7 +100,7 @@ class HomeAccessory(Accessory):
@ha_callback
def update_state_callback(self, entity_id=None, old_state=None,
new_state=None):
"""Callback from state change listener."""
"""Handle state change listener callback."""
_LOGGER.debug('New_state: %s', new_state)
if new_state is None:
return
@ -131,7 +131,7 @@ class HomeAccessory(Accessory):
hk_charging)
def update_state(self, new_state):
"""Method called on state change to update HomeKit value.
"""Handle state change to update HomeKit value.
Overridden by accessory types.
"""

View File

@ -109,7 +109,7 @@ def show_setup_message(hass, pincode):
"""Display persistent notification with setup information."""
pin = pincode.decode()
_LOGGER.info('Pincode: %s', pin)
message = 'To setup Home Assistant in the Home App, enter the ' \
message = 'To set up Home Assistant in the Home App, enter the ' \
'following code:\n### {}'.format(pin)
hass.components.persistent_notification.create(
message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID)

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.46']
REQUIREMENTS = ['pyhomematic==0.1.47']
_LOGGER = logging.getLogger(__name__)
@ -48,6 +48,8 @@ ATTR_MESSAGE = 'message'
ATTR_MODE = 'mode'
ATTR_TIME = 'time'
ATTR_UNIQUE_ID = 'unique_id'
ATTR_PARAMSET_KEY = 'paramset_key'
ATTR_PARAMSET = 'paramset'
EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse'
@ -58,6 +60,7 @@ SERVICE_RECONNECT = 'reconnect'
SERVICE_SET_VARIABLE_VALUE = 'set_variable_value'
SERVICE_SET_DEVICE_VALUE = 'set_device_value'
SERVICE_SET_INSTALL_MODE = 'set_install_mode'
SERVICE_PUT_PARAMSET = 'put_paramset'
HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [
@ -78,7 +81,7 @@ HM_DEVICE_TYPES = {
DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
'ThermostatGroup'],
'ThermostatGroup', 'IPThermostatWall230V'],
DISCOVER_BINARY_SENSORS: [
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
@ -103,7 +106,7 @@ HM_ATTRIBUTE_SUPPORT = {
'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}],
'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}],
'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}],
'RSSI_DEVICE': ['rssi', {}],
'RSSI_PEER': ['rssi', {}],
'VALVE_STATE': ['valve', {}],
'BATTERY_STATE': ['battery', {}],
'CONTROL_MODE': ['mode', {
@ -232,6 +235,13 @@ SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({
vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
})
SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema({
vol.Required(ATTR_INTERFACE): cv.string,
vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_PARAMSET_KEY): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_PARAMSET): dict,
})
@bind_hass
def virtualkey(hass, address, channel, param, interface=None):
@ -271,6 +281,19 @@ def set_device_value(hass, address, channel, param, value, interface=None):
hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data)
@bind_hass
def put_paramset(hass, interface, address, paramset_key, paramset):
"""Call putParamset XML-RPC method of supplied interface."""
data = {
ATTR_INTERFACE: interface,
ATTR_ADDRESS: address,
ATTR_PARAMSET_KEY: paramset_key,
ATTR_PARAMSET: paramset,
}
hass.services.call(DOMAIN, SERVICE_PUT_PARAMSET, data)
@bind_hass
def set_install_mode(hass, interface, mode=None, time=None, address=None):
"""Call setInstallMode XML-RPC method of supplied interface."""
@ -439,6 +462,26 @@ def setup(hass, config):
DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode,
schema=SCHEMA_SERVICE_SET_INSTALL_MODE)
def _service_put_paramset(service):
"""Service to call the putParamset method on a HomeMatic connection."""
interface = service.data.get(ATTR_INTERFACE)
address = service.data.get(ATTR_ADDRESS)
paramset_key = service.data.get(ATTR_PARAMSET_KEY)
# When passing in the paramset from a YAML file we get an OrderedDict
# here instead of a dict, so add this explicit cast.
# The service schema makes sure that this cast works.
paramset = dict(service.data.get(ATTR_PARAMSET))
_LOGGER.debug(
"Calling putParamset: %s, %s, %s, %s",
interface, address, paramset_key, paramset
)
homematic.putParamset(interface, address, paramset_key, paramset)
hass.services.register(
DOMAIN, SERVICE_PUT_PARAMSET, _service_put_paramset,
schema=SCHEMA_SERVICE_PUT_PARAMSET)
return True

View File

@ -66,3 +66,20 @@ set_install_mode:
address:
description: (Optional) Address of homematic device or BidCoS-RF to learn
example: LEQ3948571
put_paramset:
description: Call to putParamset in the RPC XML interface
fields:
interface:
description: The interfaces name from the config
example: wireless
address:
description: Address of Homematic device
example: LEQ3948571
paramset_key:
description: The paramset_key argument to putParamset
example: MASTER
paramset:
description: A paramset dictionary
example: '{"WEEK_PROGRAM_POINTER": 1}'

View File

@ -1,24 +1,23 @@
"""
Support for HomematicIP components.
Support for HomematicIP Cloud 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 homeassistant import config_entries
from homeassistant.const import CONF_NAME
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 .const import (
CONF_ACCESSPOINT, CONF_AUTHTOKEN, DOMAIN, HMIPC_AUTHTOKEN, HMIPC_HAPID,
HMIPC_NAME)
from .device import HomematicipGenericDevice # noqa: F401
from .hap import HomematicipAuth, HomematicipHAP # noqa: F401
REQUIREMENTS = ['homematicip==0.9.8']
@ -34,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({
async def async_setup(hass, config):
"""Set up the HomematicIP component."""
"""Set up the HomematicIP Cloud component."""
hass.data[DOMAIN] = {}
accesspoints = config.get(DOMAIN, [])
@ -54,7 +53,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Set up an accsspoint from a config entry."""
"""Set up an access point from a config entry."""
hap = HomematicipHAP(hass, entry)
hapid = entry.data[HMIPC_HAPID].replace('-', '').upper()
hass.data[DOMAIN][hapid] = hap

View File

@ -1,25 +1,25 @@
"""Config flow to configure HomematicIP Cloud."""
"""Config flow to configure the HomematicIP Cloud component."""
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 .const import DOMAIN as HMIPC_DOMAIN
from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN
from .const import _LOGGER
from .hap import HomematicipAuth
@callback
def configured_haps(hass):
"""Return a set of the configured accesspoints."""
"""Return a set of the configured access points."""
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."""
"""Config flow for the HomematicIP Cloud component."""
VERSION = 1
@ -44,28 +44,28 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
self.auth = HomematicipAuth(self.hass, user_input)
connected = await self.auth.async_setup()
if connected:
_LOGGER.info("Connection established")
_LOGGER.info("Connection to HomematicIP Cloud 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,
vol.Optional(HMIPC_PIN): str,
}),
errors=errors
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the HomematicIP Cloud accesspoint."""
"""Attempt to link with the HomematicIP Cloud access point."""
errors = {}
pressed = await self.auth.async_checkbutton()
if pressed:
authtoken = await self.auth.async_register()
if authtoken:
_LOGGER.info("Write config entry")
_LOGGER.info("Write config entry for HomematicIP Cloud")
return self.async_create_entry(
title=self.auth.config.get(HMIPC_HAPID),
data={
@ -73,13 +73,13 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
HMIPC_AUTHTOKEN: authtoken,
HMIPC_NAME: self.auth.config.get(HMIPC_NAME)
})
return self.async_abort(reason='conection_aborted')
return self.async_abort(reason='connection_aborted')
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."""
"""Import a new access point as a config entry."""
hapid = import_info[HMIPC_HAPID]
authtoken = import_info[HMIPC_AUTHTOKEN]
name = import_info[HMIPC_NAME]
@ -88,13 +88,13 @@ class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
if hapid in configured_haps(self.hass):
return self.async_abort(reason='already_configured')
_LOGGER.info('Imported authentication for %s', hapid)
_LOGGER.info("Imported authentication for %s", hapid)
return self.async_create_entry(
title=hapid,
data={
HMIPC_HAPID: hapid,
HMIPC_AUTHTOKEN: authtoken,
HMIPC_NAME: name
HMIPC_HAPID: hapid,
HMIPC_NAME: name,
}
)

View File

@ -14,7 +14,6 @@ COMPONENTS = [
'switch',
]
CONF_NAME = 'name'
CONF_ACCESSPOINT = 'accesspoint'
CONF_AUTHTOKEN = 'authtoken'

View File

@ -1,25 +1,25 @@
"""GenericDevice for the HomematicIP Cloud component."""
"""Generic device 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_CONNECTED = 'connected'
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_FIRMWARE_STATE = 'firmware_state'
ATTR_GROUP_TYPE = 'group_type'
ATTR_HOME_ID = 'home_id'
ATTR_HOME_NAME = 'home_name'
ATTR_LOW_BATTERY = 'low_battery'
ATTR_MODEL_TYPE = 'model_type'
ATTR_OPERATION_LOCK = 'operation_lock'
ATTR_SABOTAGE = 'sabotage'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_UNREACHABLE = 'unreachable'
class HomematicipGenericDevice(Entity):
@ -30,8 +30,7 @@ class HomematicipGenericDevice(Entity):
self._home = home
self._device = device
self.post = post
_LOGGER.info('Setting up %s (%s)', self.name,
self._device.modelType)
_LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType)
async def async_added_to_hass(self):
"""Register callbacks."""
@ -39,16 +38,16 @@ class HomematicipGenericDevice(Entity):
def _device_changed(self, json, **kwargs):
"""Handle device state changes."""
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
_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 != ''):
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 != ''):
if self.post is not None and self.post != '':
name = "{} {}".format(name, self.post)
return name

View File

@ -1,21 +1,21 @@
"""Errors for the HomematicIP component."""
"""Errors for the HomematicIP Cloud component."""
from homeassistant.exceptions import HomeAssistantError
class HmipcException(HomeAssistantError):
"""Base class for HomematicIP exceptions."""
"""Base class for HomematicIP Cloud exceptions."""
class HmipcConnectionError(HmipcException):
"""Unable to connect to the HomematicIP cloud server."""
"""Unable to connect to the HomematicIP Cloud server."""
class HmipcConnectionWait(HmipcException):
"""Wait for registration to the HomematicIP cloud server."""
"""Wait for registration to the HomematicIP Cloud server."""
class HmipcRegistrationFailed(HmipcException):
"""Registration on HomematicIP cloud failed."""
"""Registration on HomematicIP Cloud failed."""
class HmipcPressButton(HmipcException):

View File

@ -1,14 +1,13 @@
"""Accesspoint for the HomematicIP Cloud component."""
"""Access point 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 homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME,
COMPONENTS)
COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN)
from .errors import HmipcConnectionError
_LOGGER = logging.getLogger(__name__)
@ -74,10 +73,10 @@ class HomematicipAuth:
class HomematicipHAP:
"""Manages HomematicIP http and websocket connection."""
"""Manages HomematicIP HTTP and WebSocket connection."""
def __init__(self, hass, config_entry):
"""Initialize HomematicIP cloud connection."""
"""Initialize HomematicIP Cloud connection."""
self.hass = hass
self.config_entry = config_entry
self.home = None
@ -100,7 +99,7 @@ class HomematicipHAP:
except HmipcConnectionError:
retry_delay = 2 ** min(tries + 1, 6)
_LOGGER.error("Error connecting to HomematicIP with HAP %s. "
"Retrying in %d seconds.",
"Retrying in %d seconds",
self.config_entry.data.get(HMIPC_HAPID), retry_delay)
async def retry_setup(_now):
@ -113,7 +112,7 @@ class HomematicipHAP:
return False
_LOGGER.info('Connected to HomematicIP with HAP %s.',
_LOGGER.info("Connected to HomematicIP with HAP %s",
self.config_entry.data.get(HMIPC_HAPID))
for component in COMPONENTS:
@ -127,7 +126,7 @@ class HomematicipHAP:
def async_update(self, *args, **kwargs):
"""Async update the home device.
Triggered when the hmip HOME_CHANGED event has fired.
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
@ -147,7 +146,7 @@ class HomematicipHAP:
job.add_done_callback(self.get_state_finished)
async def get_state(self):
"""Update hmip state and tell hass."""
"""Update HMIP state and tell Home Assistant."""
await self.home.get_current_state()
self.update_all()
@ -161,11 +160,11 @@ class HomematicipHAP:
# Somehow connection could not recover. Will disconnect and
# so reconnect loop is taking over.
_LOGGER.error(
"updating state after himp access point reconnect failed.")
"Updating state after HMIP 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."""
"""Set all devices to unavailable and tell Home Assistant."""
for device in self.home.devices:
device.unreach = True
self.update_all()
@ -190,7 +189,7 @@ class HomematicipHAP:
return
async def async_connect(self):
"""Start websocket connection."""
"""Start WebSocket connection."""
from homematicip.base.base_connection import HmipConnectionError
tries = 0
@ -210,7 +209,7 @@ class HomematicipHAP:
tries += 1
retry_delay = 2 ** min(tries + 1, 6)
_LOGGER.error("Error connecting to HomematicIP with HAP %s. "
"Retrying in %d seconds.",
"Retrying in %d seconds",
self.config_entry.data.get(HMIPC_HAPID), retry_delay)
try:
self._retry_task = self.hass.async_add_job(asyncio.sleep(
@ -227,7 +226,7 @@ class HomematicipHAP:
if self._retry_task is not None:
self._retry_task.cancel()
self.home.disable_events()
_LOGGER.info("Closed connection to HomematicIP cloud server.")
_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)

View File

@ -3,16 +3,16 @@
"title": "HomematicIP Cloud",
"step": {
"init": {
"title": "Pick HomematicIP Accesspoint",
"title": "Pick HomematicIP Access point",
"data": {
"hapid": "Accesspoint ID (SGTIN)",
"hapid": "Access point 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)"
"title": "Link Access point",
"description": "Press the blue button on the access point 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": {
@ -23,8 +23,8 @@
},
"abort": {
"unknown": "Unknown error occurred.",
"conection_aborted": "Could not connect to HMIP server",
"already_configured": "Accesspoint is already configured"
"connection_aborted": "Could not connect to HMIP server",
"already_configured": "Access point is already configured"
}
}
}

View File

@ -232,7 +232,8 @@ class HomeAssistantHTTP:
self.is_ban_enabled = is_ban_enabled
self.ssl_profile = ssl_profile
self._handler = None
self.server = None
self.runner = None
self.site = None
def register_view(self, view):
"""Register a view with the WSGI server.
@ -308,7 +309,7 @@ class HomeAssistantHTTP:
self.app.router.add_route('GET', url_pattern, serve_file)
async def start(self):
"""Start the WSGI server."""
"""Start the aiohttp server."""
# We misunderstood the startup signal. You're not allowed to change
# anything during startup. Temp workaround.
# pylint: disable=protected-access
@ -321,7 +322,9 @@ class HomeAssistantHTTP:
context = ssl_util.server_context_intermediate()
else:
context = ssl_util.server_context_modern()
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
await self.hass.async_add_executor_job(
context.load_cert_chain, self.ssl_certificate,
self.ssl_key)
except OSError as error:
_LOGGER.error("Could not read SSL certificate from %s: %s",
self.ssl_certificate, error)
@ -329,7 +332,9 @@ class HomeAssistantHTTP:
if self.ssl_peer_certificate:
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(cafile=self.ssl_peer_certificate)
await self.hass.async_add_executor_job(
context.load_verify_locations,
self.ssl_peer_certificate)
else:
context = None
@ -340,21 +345,17 @@ class HomeAssistantHTTP:
# To work around this we now prevent the router from getting frozen
self.app._router.freeze = lambda: None
self._handler = self.app.make_handler(loop=self.hass.loop)
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.server_host,
self.server_port, ssl_context=context)
try:
self.server = await self.hass.loop.create_server(
self._handler, self.server_host, self.server_port, ssl=context)
await self.site.start()
except OSError as error:
_LOGGER.error("Failed to create HTTP server at port %d: %s",
self.server_port, error)
async def stop(self):
"""Stop the WSGI server."""
if self.server:
self.server.close()
await self.server.wait_closed()
await self.app.shutdown()
if self._handler:
await self._handler.shutdown(10)
await self.app.cleanup()
"""Stop the aiohttp server."""
await self.site.stop()
await self.runner.cleanup()

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